# Project export: TimeScribe

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: UC Berkeley AI Hackathon 2025
- Tagline: TimeScribe is a digital journal that lets you effortlessly log your thoughts. Using real-time speech transcription and seamless memory storage, TimeScribe can summarize your entire journaling history.
- Devpost: https://devpost.com/software/timescribe-zuv4c0
- GitHub: https://github.com/tednguyen1123/TimeScribe.git
- Video: https://www.youtube.com/embed/45TDC-InK28?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 2 GitHub contributor(s) — Sid Shah (14 commits), Ted Nguyen (13 commits)

## Devpost submission (written by the team)

### Inspiration

wanting to easily log our dreams to keep track of them before we forget them forever helpful for patients dealing with Alzheimer's or any dementia also helpful just for journaling thoughts and ideas that could be accessed later

### What it does

used Groq's blazingly fast latency to easily log users' thoughts by typing them into the text bar or by using the recording feature Because of Groq's speed, our product can quickly summarize and output the logged entries in between the given date range chosen by the user

### How we built it

Used Supabase to store user IDs, agent IDs, and the raw entries for each user Used Groq for STT, TTS, and to summarize the entries Used Letta to add the summarized entries to the context, which is threaded in the background

### Challenges we ran into

At first, we wanted just to use Letta Cloud to store the memory, but it took too long to retrieve the information, so we decided to store the raw data in Supabase instead We had issues trying to connect Groq and Letta, so we decided to manually send the context from Letta to Groq When trying to summarize, the context had a lot of metadata, so we had to manually filter it to only show the text of what the user logged We also ran into problems when trying to send TTS audio back to the frontend, as we did not fully understand how to format and read the HTTP requests.

### Accomplishments we're proud of

We were able to incorporate multimodality, including STT We also incorporated the summarization across customizable date ranges Our product was able to log and summarize the text relatively quickly A user can successfully access their summarized entries even when logging in on another device

### What we learned

How to work with Supabase, a database, to store user information and their data How to use Groq's quick latency to our advantage How to use Groq's whisper model to incorporate STT to transcribe the message to English text, and playai-tts to get the summarized text back to TTS Learned to integrate multiple frameworks and technologies (Groq, Letta, Supabase) Explored threading to run Letta in the background to decrease computational time

### What's next

Proper authentication systems to maintain the privacy of our users

## README (from the GitHub repository)

# TimeScribe


## Detected evidence (automated analysis)

Indexed codebase: 6 recognized source files, 19 KB.
- Flask (technology) — detected in the code
- HTML (language) — detected in the code
- JavaScript (language) — detected in the code
- Python (language) — detected in the code
- Supabase (technology) — detected in the code

## Codebase structure (from repository index)

### Files (8 of 8)

```
.gitignore
README.md
requirements.txt
timescribe-flask-project/app.py
timescribe-flask-project/letta_supabase_standalone.py
timescribe-flask-project/static/script.js
timescribe-flask-project/templates/index.html
timescribe-flask-project/templates/login.html
```

### Dependencies

- requirements.txt: flask, flask_cors, groq, letta-client, supabase

### Recent commits (newest first)

- implemented tts
- Merge branch 'main' of https://github.com/tednguyen1123/TimeScribe
- Small bug fixes
- cleaned up dependencies
- submittable draft including operational summary
- summarizeEntries frontend
- UI small fixes
- incorporated the model and changed the front end
- Letta Responses Arriving (groq not working)
- Merge branch 'main' of https://github.com/tednguyen1123/TimeScribe
- added date buttons
- agent ids implemented into app
- added STT
- added transcribe func
- Frontend Audio recording
- added voicechecker
- frontend changes for voice input
- Login sessions established ACTION: integrate with letta agent
- update requirements for flask_cors
- Merge branch 'main' of https://github.com/tednguyen1123/TimeScribe

## Key source files (fetched from GitHub, selected and truncated for size)

### requirements.txt

```
flask
flask_cors
letta-client
supabase
groq
```

### timescribe-flask-project/app.py

```python
import os, dotenv
import datetime
from flask import Flask, render_template, request, Response, session, redirect, jsonify
from flask_cors import CORS  # To handle cross-origin requests
from groq import Groq
from letta_client import Letta, MessageCreate
from supabase import create_client, Client
from threading import Thread
from concurrent.futures import ThreadPoolExecutor
import base64

executor = ThreadPoolExecutor()
dotenv.load_dotenv()
letta_client = Letta(token=os.getenv("LETTA_API_KEY"))
supabase = create_client(
    os.getenv("SUPABASE_URL"),
    os.getenv("SUPABASE_KEY")
)
groq_client = Groq(
    api_key=os.getenv("GROQ_API_KEY"),
)

app = Flask(__name__, template_folder="templates", static_folder="static")
CORS(app)  # Allow all domains for now (development only)
app.secret_key = os.getenv("FLASK_SECRET_KEY")

def store_memory(user_id: str, memory_text: str, date: str = None):
    supabase.table("Memory").insert({
        "user_id": user_id,
        "message_text": memory_text,
        "timestamp": date if date else datetime.datetime.now().strftime(r"%Y-%m-%d")
    }).execute()

def get_memories_range(user_id: str, date_start: str, date_end: str):
    result = supabase.table("Memory")\
        .select("message_text")\
        .eq("user_id", user_id)\
        .gte("timestamp", date_start)\
        .lte("timestamp", date_end)\
        .execute()
    
    return [row["message_text"] for row in result.data]

def message_filter(msg):
    if msg.role == "user":
        yield msg.content[0].text.content

def recall_context(user_id: str, query: str, letta_client: Letta, supabase, agent_id: str, date_start, date_end, limit=3):

    context = []

    try:
        letta_context = letta_client.agents.context.retrieve(agent_id=agent_id)

        # Add core memory
        if letta_context.core_memory:
            context.append(f"[Memory]\n{letta_context.core_memory}")

        # Add summary memory
        if letta_context.summary_memory:
            context.append(f"[Summary]\n{letta_context.summary_memory}")

        # Add external/archival summary
        if letta_context.external_memory_summary:
            context.append(f"[Long-term Summary]\n{letta_context.external_memory_summary}")

        # Add recent messages
        for msg in letta_context.messages:
            role = msg.role.capitalize()
            context.append(f"{role}: {msg.content}")
    except Exception as e:
        print("Letta memory search error:", e)

    try:
        supabase_context = get_memories_range(
            user_id=user_id,
            date_start=date_start,
            date_end=date_end
        )
        context.extend(supabase_context)
    except Exception as e:
        print("Supabase memory fetch error:", e)
    return context


@app.route("/")
def index():
    user_id = session.get("user_id")
    if not user_id:
        return redirect("/login")
    else:
        print(f"User ID: {user_id} is logged in")
        # Check if this user already has an agent
        existing = supabase.table("agent_ids").select("*").eq("user_id", user_id).execute()
        if existing.data:
            agent_id = existing.data[0]['agent_id']
            print(f"Agent already exists for {user_id}: {agent_id}")
        else:
            agent = letta_client.agents.create(
                model="anthropic/claude-3-5-haiku",
                embedding="openai/text-embedding-3-small",
                memory_blocks=[
                    {"label": "human", "value": f"User name: {user_id}"},
                    {"label": "persona", "value": "You are a memory journal keeper, helping users track their thoughts and experiences. Only respond with 'logged entry for {date}' when the user inputs a message."}
                ]
            )
            agent_id = agent.id
            supabase.table("agent_ids").insert({
                "user_id": user_id,
                "agent_id": agent_id
            }).execute()
            print(f"Created new agent for {user_id}: {agent_id}")
        session["agent_id"] = agent_id
        return render_template("index.html")

@app.route("/login", methods=["GET", "POST"])
def login():
    if request.method == "POST":
        # Handle login logic here
        user_id = request.form.get("user_id")
        if user_id:
            session["user_id"] = user_id
            return redirect("/")
        else:
            return "Please provide a user ID", 400
    # Render a simple login page
    else:
        return render_template("login.html")

@app.route("/chat", methods=["POST"])
async def chat():
    data = request.get_json()
    user_input = data.get("message", "")
    datetime_now = datetime.datetime.now().strftime(r"%Y-%m-%d")
    user_input = f"{datetime_now} \n\n{user_input}"

    if not user_input:
        return {"error": "No message provided"}, 400

    agent_id = session.get("agent_id")

    # Wrap synchronous Letta call into coroutine
    try:
        store_memory(
            user_id=session.get("user_id"),
            memory_text=user_input,
            date=datetime_now
        )
        # add to letta memory 
        response = groq_client.chat.completions.create(
            model="llama-3.1-8b-instant",
            messages=[
                {"role": "system", "content": "You are helping people record their memories and thoughts."},
                {"role": "user", "content": f"Give me the important details from this, in a short message: {user_input}"}
            ],
            temperature=0.0
        )
        print(response.choices[0].message.content)
        n = datetime.datetime.now()
        Thread(target=letta_client.agents.messages.create,kwargs={
            "agent_id": agent_id,
            "messages": [MessageCreate(
                role="user",
                content=response.choices[0].message.content
        )]}).start()
        print(datetime.datetime.now() - n)
        
    except Exception as e:
        print("❌ Letta error:", e)
        return jsonify({"error": str(e)}),
[truncated — 3145 more characters]
```

### timescribe-flask-project/letta_supabase_standalone.py

```python
import os, dotenv
from letta_client import Letta
from supabase import create_client, Client

dotenv.load_dotenv()
client = Letta(token=os.getenv("LETTA_API_KEY"))
supabase = create_client(
    os.getenv("SUPABASE_URL"),
    os.getenv("SUPABASE_KEY")
)
name = "Sid"  # or from signup/login system

# Check if this user already has an agent
existing = supabase.table("agent_ids").select("*").eq("user_id", name).execute()
if existing.data:
    agent_id = existing.data[0]['agent_id']
    print(f"Agent already exists for {name}: {agent_id}")
else:
    agent = client.agents.create(
        model="openai/gpt-4",
        embedding="openai/text-embedding-3-small",
        memory_blocks=[
            {"label": "human", "value": f"User name: {name}"},
            {"label": "persona", "value": "You are a memory journal keeper, helping users track their thoughts and experiences."}
        ]
    )
    agent_id = agent.id
    supabase.table("agent_ids").insert({
        "user_id": name,
        "agent_id": agent_id
    }).execute()
    print(f"Created new agent for {name}: {agent_id}")
```

### timescribe-flask-project/templates/login.html

```html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>TimeScribe</title>
    <style>
        body {
          font-family: 'Inter', sans-serif;
          background-color: #f7f7fa;
          color: #333;
          max-width: 600px;
          margin: 40px auto;
          padding: 20px;
          border-radius: 12px;
          box-shadow: 0 0 12px rgba(0, 0, 0, 0.1);
        }
        h1 {
          text-align: center;
          margin-bottom: 24px;
          color: #2c3e50;
        }
        #chat-box {
          background-color: #ffffff;
          border: 1px solid #ddd;
          padding: 15px;
          border-radius: 8px;
          height: 250px;
          overflow-y: auto;
          margin-bottom: 20px;
        }
        input[type="text"],
        input[type="date"] {
          padding: 10px;
          font-size: 16px;
          margin: 8px 0;
          width: calc(100% - 22px);
          border: 1px solid #ccc;
          border-radius: 6px;
        }
        .danger {
          background-color: #e74c3c;
        }
        .danger:hover {
          background-color: #c0392b;
        }
        button {
          padding: 10px 16px;
          margin: 6px 4px 12px 0;
          border: none;
          background-color: #4a90e2;
          color: white;
          border-radius: 6px;
          cursor: pointer;
          font-size: 14px;
        }
        button:hover {
          background-color: #357abd;
        }
        .controls {
          display: flex;
          flex-wrap: wrap;
          gap: 10px;
          margin-bottom: 12px;
        }
        .date-range {
          display: flex;
          flex-direction: column;
          gap: 8px;
          margin: 12px 0;
        }
        label {
          font-size: 14px;
          margin-top: 4px;
        }
        #voice-output {
          margin-right: 6px;
        }
        .inline {
          display: flex;
          flex-wrap: row wrap;
          align-items: center;
          gap: 10px;
        }
        .inline button {
          text-wrap: nowrap;
          margin: 0;
        }
        .inline input[type="text"] {
          flex-grow: 1;
        }
      </style>
  </head>
  <body>
    <h1>TimeScribe Login</h1>
    <form action="/login" method="post" class="inline">
      <!-- <label for="username">Username:</label> -->
      <input type="text" id="user_id" name="user_id" required placeholder="User ID"/>
      <button type="submit">Login</button>
    </form>

    <script src="/static/script.js"></script>
  </body>
</html>

```

### timescribe-flask-project/templates/index.html

```html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>TimeScribe</title>
    <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Inter:400,600&display=swap" />
    <style>
      body {
        font-family: 'Inter', sans-serif;
        background-color: #f7f7fa;
        color: #333;
        max-width: 600px;
        margin: 40px auto;
        padding: 20px;
        border-radius: 12px;
        box-shadow: 0 0 12px rgba(0, 0, 0, 0.1);
      }
      h1 {
        text-align: center;
        margin-bottom: 24px;
        color: #2c3e50;
      }
      #chat-box {
        background-color: #ffffff;
        border: 1px solid #ddd;
        padding: 15px;
        border-radius: 8px;
        height: 250px;
        overflow-y: auto;
        margin-bottom: 20px;
      }
      input[type="text"],
      input[type="date"] {
        padding: 10px;
        font-size: 16px;
        margin: 8px 0;
        width: calc(100% - 22px);
        border: 1px solid #ccc;
        border-radius: 6px;
      }
      .danger {
        background-color: #e74c3c;
      }
      .danger:hover {
        background-color: #c0392b;
      }
      button {
        padding: 10px 16px;
        margin: 6px 4px 12px 0;
        border: none;
        background-color: #4a90e2;
        color: white;
        border-radius: 6px;
        cursor: pointer;
        font-size: 14px;
      }
      button:hover {
        background-color: #357abd;
      }
      .controls {
        display: flex;
        flex-wrap: wrap;
        gap: 10px;
        margin-bottom: 12px;
      }
      .date-range {
        display: flex;
        flex-direction: column;
        gap: 8px;
        margin: 12px 0;
      }
      label {
        font-size: 14px;
        margin-top: 4px;
      }
      #voice-output {
        margin-right: 6px;
      }
      .inline {
        display: flex;
        flex-wrap: row wrap;
        align-items: center;
        gap: 10px;
      }
      .inline button {
        text-wrap: nowrap;
        margin: 0;
      }
      .inline input[type="text"] {
        flex-grow: 1;
      }
    </style>
  </head>
  <body>
    <h1>TimeScribe</h1>
    <div id="chat-box"></div>
    <div id="listening-box"></div>
      <form class="inline" onsubmit="return false;">
        <input type="text" id="user-input" placeholder="Start logging..." /><button type="submit" onclick="sendMessage()">Send</button><br>
      </form>
    <button onclick="startRecordingLive()">Start Recording 🎤</button>
    <button onclick="stopRecordingLive()">Stop Recording 🛑</button><br>
    <input type="checkbox" id="voice-output" /><label for="voice-output">Enable Voice Output </label>
    <p></p>
    <!--ACTION: Add Summarize command-->
    <!--ACTION: Add Recall command-->
    <!--ACTION: Add Speak command-->
    <br>
    
    <form class="inline" onsubmit="return false;">

      <label for="date-input">Start Date:</label>
      <input type="date" id="date-start" name="entry-date" required />
      <label for="date-input">End Date:</label>
      <input type="date" id="date-end" name="entry-date" required />
      <button type="submit" onclick="summarizeEntries()">Summarize Entries</button>
    </form><br>


      <button onclick="window.location.href='/login'" name="logout" class="danger">Logout</button>   
      <script src="/static/script.js"></script>
  </body>
</html>

```

### timescribe-flask-project/static/script.js

```javascript
let mediaRecorder;
let audioChunks = [];

function startRecordingLive() {
  const listeningBox = document.getElementById("listening-box");

  listeningBox.innerText = "🎙 Listening...";
  audioChunks = [];

  navigator.mediaDevices.getUserMedia({ audio: true }).then(stream => {
    mediaRecorder = new MediaRecorder(stream);
    mediaRecorder.start();

    mediaRecorder.ondataavailable = (e) => {
      audioChunks.push(e.data);
    };

    mediaRecorder.onstop = async () => {
      const fullBlob = new Blob(audioChunks, { type: "audio/webm" });

      const formData = new FormData();
      formData.append("audio", fullBlob, "recording.webm");

      try {
        const res = await fetch("/transcribe", {
          method: "POST",
          body: formData
        });

        const data = await res.json();
        const transcript = data.transcription;
        console.log("Transcription:", transcript);

      // Pre-fill input box (but don't send it yet)
        document.getElementById("user-input").value = transcript;
        sendMessage();

      } catch (err) {
        console.error("Transcription failed:", err);
        listeningBox.innerText = "⚠️ Transcription failed.";
      }

      listeningBox.innerText = "";
    };
  })
    .catch(err => {
      console.error("Microphone access denied or failed:", err);
      listeningBox.innerText = "🎙 Microphone not available.";
    });
}

function stopRecordingLive() {
  const listeningBox = document.getElementById("listening-box");
  listeningBox.innerText = "";
  if (mediaRecorder && mediaRecorder.state !== "inactive") {
    mediaRecorder.stop();
  }
  listeningBox.innerText = "";
}

async function sendMessage() {
  const input = document.getElementById("user-input");
  const chatBox = document.getElementById("chat-box");
  const dateInput = document.getElementById("date-start");


  const message = input.value.trim();
  if (!message) return;

  chatBox.innerHTML += `<p><b>You:</b> ${message}</p>`;
  const date = dateInput?.value;
  input.value = "";


  body = { ...{ message } };
  const response = await fetch("/chat", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(body)
  });

  const data = await response.json();
  console.log("AI Response:", data);
  chatBox.innerHTML += `<p><b>AI:</b> ${data.response}</p>`;
}

function summarizeEntries() {
  const chatBox = document.getElementById("chat-box");
  const dateStart = document.getElementById("date-start");
  const dateEnd = document.getElementById("date-end");


  chatBox.innerHTML += `<p><b>Summarizing entries from </b> ${dateStart.value} <b>to</b> ${dateEnd.value}</p>`;
  const voiceOn = document.getElementById("voice-output").checked;

  body = { date_start: dateStart.value, date_end: dateEnd.value, voice_on: voiceOn};
  fetch("/summarize", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(body)
  })
  .then(res => res.json())
  .then(data => {
    if (voiceOn) {
      const byteChars = atob(data.audio_data);
      const byteNumbers = new Array(byteChars.length);
      for (let i = 0; i < byteChars.length; i++) {
        byteNumbers[i] = byteChars.charCodeAt(i);
      }
      const byteArray = new Uint8Array(byteNumbers);
      const blob = new Blob([byteArray], { type: data.mime_type });
      const audioUrl = URL.createObjectURL(blob);
      const audio = new Audio(audioUrl);
      audio.play();
    }
    chatBox.innerHTML += `<p><b>Summary:</b> ${data.summary}</p>`;
  });
}
```