# Project export: HeartHack

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: TreeHacks 2025
- Tagline: Stop wasting your time swiping! Our AI-powered tool analyzes Tinder profiles, matches them to your preferences, and automates swipes—saving you time and bringing you closer to the right match.
- Devpost: https://devpost.com/software/hearthack
- GitHub: https://github.com/avah1026/treehacks-2025
- Video: https://www.youtube.com/embed/IzOnzsCE8pU?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 2 GitHub contributor(s) — Bartek Pacia (5 commits), Ava Harrington (3 commits)

## Devpost submission (written by the team)

### Overview

Making soul-sucking dating apps suck even more, one match at a time! HeartHack isn’t just about automating dating—it’s about making dating smarter, faster, and more intuitive. HeartHack is totally about automating dating, and making it even less personal and soulless!

### Inspiration

"In today’s fast-paced world, spending hours mindlessly swiping on dating apps feels like a chore. Finding love shouldn’t be exhausting—so we built a smarter solution. Everyone knows what they’re looking for, but sifting through endless profiles? That’s the real challenge. With HeartHack, you can skip the tedious search and jump straight to meaningful matches." Actually, it just seemed like an idea with quite a big LOL potential. What It Does HeartHack is an AI-powered tool that automates swiping on dating apps based on your personal preferences. First, it asks you a few key questions to understand your ideal match and builds a personalized profile. From there, it scans Tinder profiles from screenshots, using LLM (currently Gemini, but this could be easily changed) to analyze the bios and photos of potential matches. If a profile aligns with your preferences, it swipes right; if it detects dealbreakers, it swipes left. How We Built It HeartHack consists of 2 parts: "gatherer" – the web app where you provide information about your dating preferences "swiper" – a CLI app running an HTTP server that receives info from the gatherer, and then interacts with a connected Android device (opens up Tinder, browses through profiles and swipes) Technologies used: HTML, CSS, JavaScript (with React) for the "gatherer" web app Kotlin (programming language) + Maestro (mobile app automation framework) in the "swiper" CLI app Gemini Mobile App Automation: To interact with Tinder (or any other app on a mobile device), we use parts of the Maestro framework built by Mobile.dev. Maestro lets us interact with the mobile Android or iOS device just like a real user does – we can tap, perform gestures, input text, open different apps, and capture screenshots. AI-Powered Matching: Once the screenshots are saved, Gemini Flash 2.0 analyzes bios and photos to determine compatibility. Profiles that meet the user’s criteria get a right swipe, while those with red flags get a left swipe. Challenges We Ran Into We initially built the "gatherer" app in Python (using PyQT), but quickly realized that using web technologies for it would be more fun, flexible, and also a good learning experience. Later, we hit another roadblock when we realized that the Maestro framework is really laser-focused on testing mobile apps and doesn't lend itself to general-purpose mobile device automation which we actually needed. To overcome this, we extracted some of its internals and used them directly, omitting parts we didn't need. Accomplishments We’re Proud Of We are happy we managed to build a functional system that works quite reliably (by hackathon project standards). Successfully integrating together the few (quite different) technologies we used under such tight time constraints and then seeing it work felt really good! What We Learned This project pushed us to explore a few new technologies—React, the Kotlin language, Gemini API, and mobile app automation with Maestro—that some of us had never used before. Each team member took on the challenge of learning something unfamiliar, and for two of us, it was our first-ever hackathon. The experience gave us a crash course in rapid problem-solving, teamwork, and working under tight deadlines. Plus, we had the chance to learn directly from a more experienced programmer, gaining more confidence in working with different parts of a quite simple but not-so-typical software system. Possible future improvements Adaptive Swiping: We aim to track user swiping history to refine our AI’s decision-making, making matches even more tailored. Adaptive Swiping: We aim to track user swiping history to refine our AI’s decision-making, making matches even more tailored. More dating apps: it'd be easy to add support for more dating apps, like Bumble or Hinge. More dating apps: it'd be easy to add support for more dating apps, like Bumble or Hinge. Smart Conversations: if we had more time we'd also add LLM-powered conversations, so you didn't have to even spend time to rizz up your matches to go on a date with you. Smart Conversations: if we had more time we'd also add LLM-powered conversations, so you didn't have to even spend time to rizz up your matches to go on a date with you. Your personal dating farm: nothing is stopping us from running a few Android emulators at once, to get even more matches. Couple this with LLM-generated conversation and BAM! you're making the dating app experience suck even more for everyone, even more efficiently! :D Your personal dating farm: nothing is stopping us from running a few Android emulators at once, to get even more matches. Couple this with LLM-generated conversation and BAM! you're making the dating app experience suck even more for everyone, even more efficiently! :D PS Funny thing: our very clearly garbage, fake profile hasn't been banned by Tinder after 2 days of swiping:)

## README (from the GitHub repository)

# HeartHack

> Making the soul-sucking dating apps suck even more, one swipe at a time!

Stop wasting time on swiping. Simply describe what are your green and red flags
in a potential match, and let HeartHack swipe for you.

**Yes it's clearly for-the-lols project, but akshually it does solve a specific problem (kind of):**

By default, most dating apps let you specify only very basic filtering criteria:
age and gender. If you want anything more fine-grained (like education, family
plans, hell even covid vaccine), you have to bite the bullet and pay up. But
even this won't save you from seeing all those shirtless pics 🥵 This is why we
built HeartHack.

### Possible future improvements

devpost link: TODO

Authors:
- [Alexis Tuchnida](https://github.com/AlexisTuchinda)
- [Ava Harrington](https://github.com/avah1026)
- [Bartek Pacia](https://github.com/bartekpacia)


## Detected evidence (automated analysis)

Indexed codebase: 10 recognized source files, 22 KB.
- CSS (language) — detected in the code
- HTML (language) — detected in the code
- JavaScript (language) — detected in the code
- Kotlin (language) — detected in the code
- React (technology) — detected in the code
- Google Gemini (technology) — claimed on Devpost, not found in the code

## Codebase structure (from repository index)

### Files (20 of 20)

```
.gitignore
gatherer/.gitignore
gatherer/eslint.config.mjs
gatherer/index.html
gatherer/package.json
gatherer/src/App.jsx
gatherer/src/ProfileCreator.jsx
gatherer/src/styles.css
gatherer/vite.config.js
notes.txt
README.md
swiper/.gitignore
swiper/build.gradle.kts
swiper/gradle.properties
swiper/gradle/wrapper/gradle-wrapper.properties
swiper/gradlew
swiper/gradlew.bat
swiper/settings.gradle.kts
swiper/src/main/kotlin/tech/pacia/tinderswiper/gemini.kt
swiper/src/main/kotlin/tech/pacia/tinderswiper/main.kt
```

### Dependencies

- gatherer/package.json: @eslint/js@^9.20.0, @vitejs/plugin-react@4.3.1, eslint@9.9.1, eslint-config-prettier@9.1.0, eslint-plugin-react@7.37.1, globals@15.9.0, prettier@3.3.3, react@18.3.1, react-dom@18.3.1, vite@5.4.2

### Recent commits (newest first)

- readme update
- more stuffies yay
- run prettier, eslint, webstorm fixes (#3)
- Gemini stuff add (#2)
- Merge stuffs! yeeeee (#1)
- Create Send Data button
- Add style features
- Initial Commit

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

### gatherer/package.json

```
{
  "name": "citr-v9-projects",
  "version": "1.0.0",
  "main": "index.js",
  "scripts": {
    "build": "vite build",
    "dev": "vite",
    "format": "prettier --write \"src/**/*.{js,jsx}\"",
    "lint": "eslint",
    "preview": "vite preview"
  },
  "keywords": [],
  "type": "module",
  "author": "Brian Holt",
  "license": "Apache-2.0",
  "description": "The Complete Intro to React v9, as taught by Brian Holt for Frontend Masters",
  "devDependencies": {
    "@eslint/js": "^9.20.0",
    "@vitejs/plugin-react": "4.3.1",
    "eslint": "9.9.1",
    "eslint-config-prettier": "9.1.0",
    "eslint-plugin-react": "7.37.1",
    "globals": "15.9.0",
    "prettier": "3.3.3",
    "vite": "5.4.2"
  },
  "dependencies": {
    "react": "18.3.1",
    "react-dom": "18.3.1"
  }
}

```

### gatherer/src/App.jsx

```javascript
import { createRoot } from "react-dom/client";
import ProfileCreator from "./ProfileCreator";

const App = () => {
  return (
    <div>
      <ProfileCreator />
    </div>
  );
};

const container = document.getElementById("root");
const root = createRoot(container);
root.render(<App />);

```

### swiper/settings.gradle.kts

```kotlin
rootProject.name = "treehacks25"

```

### gatherer/vite.config.js

```javascript
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";

// https://vitejs.dev/config/
export default defineConfig({
  server: {
    proxy: {
      "/api": {
        target: "http://localhost:3000",
        changeOrigin: true,
      },
      "/public": {
        target: "http://localhost:3000",
        changeOrigin: true,
      },
    },
  },
  plugins: [react()],
});

```

### gatherer/index.html

```html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>HeartHack</title>
    <link rel="stylesheet" href="./src/styles.css" />
  </head>

  <body>
    <div id="root">not rendered</div>
    <script type="module" src="./src/App.jsx"></script>
  </body>
</html>

```

### swiper/build.gradle.kts

```kotlin
plugins {
    application
    id("org.jetbrains.kotlin.jvm") version "2.1.0"
    id("org.jetbrains.kotlin.plugin.serialization") version "2.1.0"
}

group = "tech.pacia.tinderswiper"
version = "1.0-SNAPSHOT"

repositories {
    mavenCentral()
}

application {
    mainClass = "tech.pacia.tinderswiper.MainKt"
}

kotlin {
    jvmToolchain(23)
}

dependencies {
    implementation("dev.mobile:maestro-client:1.39.13")
    implementation("dev.mobile:maestro-utils:1.39.13") // required because Metrics
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.1")

    implementation("io.ktor:ktor-server-core-jvm:2.3.12")
    implementation("io.ktor:ktor-server-cio:2.3.12") // use CIO instead of Netty. Netty gets duplicated and breaks Maestro's gRPC :(
    // implementation("io.ktor:ktor-server-netty:2.3.12") // don't use netty, it breaks Maestro's gRPC :(
    // implementation("dev.mobile:maestro-orchestra:1.39.13")
    implementation("dev.mobile:maestro-ios:1.39.13") // required because has SLF4J?

    implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.0")
    // implementation("com.google.ai.client.generativeai:generativeai:0.9.0")
    implementation("io.ktor:ktor-server-cors:2.3.12")
    implementation("dev.shreyaspatil.generativeai:generativeai-google:0.9.0-1.0.1")
    // implementation("com.google.generativeai:generativeai:0.1.1")

    implementation("ch.qos.logback:logback-classic:1.5.16")

    testImplementation(kotlin("test"))
}

tasks.test {
    useJUnitPlatform()
}

```

### gatherer/src/styles.css

```css
/* Color variables */
:root {
    --bg-color: #f2e3e3;
    --text-color: #2C3E50;
    --accent-color: #E74C3C;
    --button-color: #E9967A;
    --button-text: #1a1a1a;
    --input-bg: white;
    --success-color: #2ECC71;
    --border-color: #34495E;
    --hover-color: #ff6f61;
}

/* Main container styles */
.container {
    /* width: 900px;
    height: 1000px;
    background-color: var(--bg-color);
    padding: 40px 30px; */
    max-width: 1200px;
    width: 90%;
    margin: auto;
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
}

/*responsice adjustments for tablets/smaller screens*/
@media (max-width: 1024px) {
    .container {
        flex-direction: column;
        align-items: center;
    }
}

/*responsive adjustments for phones*/
@media (max-width: 768px) {
    .container {
        width: 100%;
        padding: 10px;
    }

    .profile-card {
        width: 80%;
    }
}

/* Header styles */
.header {
    font-family: "Courier New", Courier, monospace;
    font-size: 35px;
    font-weight: bold;
    color: #E9967A;
    text-align: center;
    margin-bottom: 20px;
    padding-bottom: 10px;
    border-bottom: 1px solid var(--border-color);
}

/* Question styles */
.question-label {
    font-family: 'Comfortaa', sans-serif;
    font-size: 16px;
    color: black;
    margin-bottom: 20px;
    padding-bottom: 6px;
    display: block;
}

.button-frame {
    display: flex;
    gap: 0.5em;
}

/* Button styles */
.button {
    font-family: 'Courier New', monospace;
    font-size: 13px;
    font-weight: bold;
    background-color: var(--button-color);
    color: var(--button-text);
    border: none;
    padding: 2px 8px;
    cursor: pointer;
    width: 120px;
    height: 30px;
    transition: all 0.3s ease;
    box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}

.button:hover {
    background-color: var(--hover-color);
    transform: translateY(-3px);
}

.button:focus,
.button:active {
    background-color: var(--button-color);
    outline: none;
}

/* Text input styles */
.text-input {
    font-family: 'Comfortaa', sans-serif;
    font-size: 20px;
    background-color: var(--input-bg);
    color: black;
    padding: 15px;
    border: 1px solid var(--border-color);
    width: 100%;
    height: 120px;
    resize: none;
    border-radius: 15px;
    transition: border 0.3s ease;
}

.text-input:focus {
    border-color: var(--accent-color);
    outline: none;
}

/* Items list styles */
.items-list {
    font-family: 'Comfortaa', sans-serif;
    font-size: 14px;
    color: black;
    margin-top: 20px;
}

.items-list li {
    list-style-type: none;
    padding-left: 1em;
    position: relative;
    margin-bottom: 10px;
}

.items-list li::before {
    content: "•";
    position: absolute;
    left: 0;
    color: var(--accent-color);
}

.profile-card {
    border-radius: 15px;
    box-shadow: 0 6px 15px rgba(0, 0, 0, 0.1); /*light shadow for depth*/
    padding: 50px;
    max-width: 600px;
    margin: 20px;
    transition: transform 0.3s ease, box-shadow 0.3s ease;
}

/*profile animation*/
@keyframes revealProfileCard {
    0% {
        opacity: 0;
        transform: translateY(20px);
    }
    100% {
        opacity: 1;
        transform: translateY(0);
    }
}

.profile-summary {
    animation: revealProfileCard 0.6s ease-out;
}


```

### gatherer/src/ProfileCreator.jsx

```javascript
import { useState } from "react";

const questions = [
  "What's your username?",
  "What are you looking for in a match? (green flags)",
  "What are your deal breakers? (red flags)",
  "What are your interests/hobbies?",
];

const ProfileCreator = () => {
  const [currentSection, setCurrentSection] = useState(0);

  const [responses, setResponses] = useState({
    username: "",
    lookingFor: [],
    dealBreakers: [],
    interests: [],
    ageRange: "",
    locationPreference: "",
  });

  const [questioningComplete, setQuestioningComplete] = useState(false);
  const [currentInput, setCurrentInput] = useState("");
  //const history = useHistory(); //history for navigation

  const handleNext = () => {
    switch (currentSection) {
      case 0: // Username
        if (currentInput) {
          setResponses((prev) => ({ ...prev, username: currentInput }));
          setCurrentSection(1);
          setCurrentInput("");
        }
        break;
      case 1: // Looking for
      case 2: // Deal breakers
      case 3: {
        // Interests
        const key = ["lookingFor", "dealBreakers", "interests"][
          currentSection - 1
        ];
        if (currentSection === 3) {
          setQuestioningComplete(true);
        } else responses[key].length > 0;
        {
          setCurrentSection((prev) => prev + 1);
          setCurrentInput("");
        }
        break;
      }
      // ... handle other sections
      default:
        break;
    }
  };

  const handlePrevious = () => {
    if (currentSection > 0) {
      setCurrentSection((prev) => prev - 1);

      if (currentSection === 4) {
        setQuestioningComplete(false);
      }
    }
  };

  const addItem = () => {
    if (!currentInput.trim()) return;

    const key = ["lookingFor", "dealBreakers", "interests"][currentSection - 1];
    const cleanedInput = currentInput.trim().replace(/^•\s*/, "");

    setResponses((prev) => ({
      ...prev,
      [key]: [...prev[key], cleanedInput],
    }));
    setCurrentInput("• ");
  };

  const sendData = async () => {
    try {
      const response = await fetch("http://localhost:8080/my-data", {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
        },
        body: JSON.stringify(responses),
      });

      const result = await response.json();
      console.log("Response:", result);
    } catch (error) {
      console.error("Error:", error);
    }
  };

  return (
    <div className="container">
      <div className="profile-card">
        <h1 className="header">HeartHack Profile</h1>

        <div className="question-frame">
          <label className="question-label">{questions[currentSection]}</label>

          {/* conditionally render profile summary -- no textbox for this portion */}
          {questioningComplete ? (
            <div className="profile-summary">
              <h2>Profile saved successfully!🥳</h2>
              <p>Username: {responses.username}</p>
              <p>Looking For: {responses.lookingFor?.join(", ")}</p>
              <p>Deal Breakers: {responses.dealBreakers?.join(", ")}</p>
              <p>Interests: {responses.interests?.join(", ")}</p>
            </div>
          ) : (
            <>
              <textarea
                autoComplete="off"
                className="text-input"
                value={currentInput}
                onChange={(e) => setCurrentInput(e.target.value)}
                onKeyPress={(e) => {
                  if (e.key === "Enter") {
                    e.preventDefault();
                    if ([1, 2, 3].includes(currentSection)) {
                      addItem();
                    } else {
                      handleNext();
                    }
                  }
                }}
              />

              <ul className="items-list">
                {[1, 2, 3].includes(currentSection) &&
                  responses[
                    ["lookingFor", "dealBreakers", "interests"][
                      currentSection - 1
                    ]
                  ].map((item, index) => <li key={index}>{item}</li>)}
              </ul>
            </>
          )}
          <div className="button-frame">
            {/* render "next" button when questioning is not complete */}
            {[1, 2, 3].includes(currentSection) && (
              <button className="button" onClick={addItem}>
                Add Item
              </button>
            )}
            <button
              className="button"
              onClick={currentSection === 4 ? sendData : handleNext}
            >
              {currentSection === 4 ? "Send Data" : "Next"}
            </button>
            {currentSection > 0 && (
              <button className="button" onClick={handlePrevious}>
                Back
              </button>
            )}
          </div>
        </div>
      </div>
    </div>
  );
};

export default ProfileCreator;

```

### swiper/src/main/kotlin/tech/pacia/tinderswiper/main.kt

```kotlin
package tech.pacia.tinderswiper

import dadb.Dadb
import io.ktor.http.ContentType
import io.ktor.http.HttpHeaders
import io.ktor.server.application.call
import io.ktor.server.application.install
import io.ktor.server.cio.CIO
import io.ktor.server.engine.embeddedServer
import io.ktor.server.plugins.cors.routing.CORS
import io.ktor.server.request.receiveText
import io.ktor.server.response.respondText
import io.ktor.server.routing.post
import io.ktor.server.routing.routing
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.json.Json
import maestro.Maestro
import maestro.drivers.AndroidDriver
import okio.Buffer
import java.io.IOException
import java.util.concurrent.CancellationException
import java.util.concurrent.TimeoutException

private val json = Json { ignoreUnknownKeys = true }

private lateinit var maestro: Maestro
private lateinit var gemini: Gemini
private var myOwnPreferences: MyOwnPreferences? = null

fun main() = runBlocking {
    val geminiApiKey = System.getenv("GEMINI_API_KEY") ?: throw IllegalStateException("GEMINI_API_KEY not set")
    gemini = Gemini(apiKey = geminiApiKey)

    val server = embeddedServer(CIO, 8080) {
        install(CORS) {
            anyHost() // Allows requests from any origin
            allowHeader(HttpHeaders.ContentType)
            allowHeader(HttpHeaders.Authorization)
            allowCredentials = true
            allowNonSimpleContentTypes = true
            allowSameOrigin = true
            // anyMethod() // Allows all HTTP methods
        }

        routing {
            post("/my-data") {
                val jsonStr = call.receiveText()
                myOwnPreferences = json.decodeFromString<MyOwnPreferences>(jsonStr)

                log("Received data! MyOwnPreferences: $myOwnPreferences")

                val responseJSON = "\"msg\": \"You submitted data!\""
                call.respondText(responseJSON, contentType = ContentType.Application.Json)
            }
        }
    }

    val serverJob = launch {
        server.start(wait = false)
    }

    while (myOwnPreferences == null) {
        log("Did not receive user's preferences yet, waiting for it...")
        delay(1000)
    }

    log("Received user's preferences! Will start the main loop.")
    mainLoop()
    log("Main loop done!")
    serverJob.cancel(CancellationException("Main loop is done, server has no purpose to live anymore"))
}

private suspend fun mainLoop() {
    log("launching...")

    val dadb = Dadb.create("localhost", 5555)
    val driver = AndroidDriver(dadb = dadb)

    delay(timeMillis = 1000L)

    maestro = try {
        Maestro.android(driver)
    } catch (exception: IOException) {
        log("Error: IO exception!")
        exception.printStackTrace()
        return
    } catch (exception: TimeoutException) {
        log("Error: timed out!")
        exception.printStackTrace()
        return
    }

    log("ready")

    maestro.launchApp("com.tinder", stopIfRunning = false)

    log("launched the app, now will wait for 3s")
    delay(timeMillis = 3000L)
    log("done waiting for 3s!")

    var currentProfileIndex = 0 // TODO: Loop infinitely until the "you're out of likes" message appears
    val profilesToSee = 10
    (0..<profilesToSee).forEach { profileIndex ->
        val photos = mutableListOf<Buffer>()
        (0..<3).forEach { i ->
            // TODO: Loop over all images in one's profile, not just 3
            val imageBuffer = screenshot()
            photos.add(imageBuffer)
            nextPic()
        }

        val verdict = gemini.analyzeProfile(
            myOwnPrefs = myOwnPreferences!!,
            images = photos,
        )
        val like = verdict == 1 /* Random.nextBoolean() */
        log("Send a like? $like")
        if (like) {
            yay()
        } else {
            nay()
        }
    }

    log("before close???")

    maestro.close()
}

private fun screenshot(): Buffer {
    log("will take a screenshot")

    val buffer = Buffer()
    maestro.takeScreenshot(buffer, false)

    return buffer
}

private suspend fun yay() {
    log("will swipe right :(")
    maestro.swipe(startRelative = "20%,50%", endRelative = "80%,50%", duration = 1000)

    log("swiped right, now will wait 1s")
    delay(timeMillis = 1000L)
}

private suspend fun nay() {
    log("will swipe left :(")
    maestro.swipe(startRelative = "80%,50%", endRelative = "20%,50%", duration = 1000)

    log("swiped left, now will wait 1s")
    delay(timeMillis = 1000L)
}

private suspend fun nextPic() {
    log("will tap to see the next pic")
    maestro.tapOnRelative(percentX = 80, percentY = 50, retryIfNoChange = false)

    delay(timeMillis = 1000L)
}

private fun log(message: Any?) {
    println("treehacks: $message")
}

```

### swiper/src/main/kotlin/tech/pacia/tinderswiper/gemini.kt

```kotlin
package tech.pacia.tinderswiper

import dev.shreyaspatil.ai.client.generativeai.GenerativeModel
import dev.shreyaspatil.ai.client.generativeai.type.content
import kotlinx.serialization.Serializable
import okio.Buffer
import java.io.FileNotFoundException

@Serializable
data class MyOwnPreferences(
    val lookingFor: List<String>,
    val dealBreakers: List<String>,
    val interests: List<String>,
    val ageRange: String,
    val locationPreference: String,
)

data class ExternalProfile(
    val bio: String,
    val imageDescriptions: List<String>,
)

class Gemini(private val apiKey: String) {

    // ... existing code ...

    suspend fun compareProfiles(myOwnPrefs: MyOwnPreferences, externalProfile: ExternalProfile): Int {
        try {
            // val preferencesFile = File("user_preferences.json").readText()
            // val preferences = Json.decodeFromString<Map<String, MyOwnPreferences>>(preferencesFile)
            // val userPrefs = preferences.values.first()

            // Create prompt for Gemini to analyze compatibility
            val prompt = """
                Analyze this dating profile against the user's preferences. Rate compatibility from 0-1.
                
                Profile Bio: ${externalProfile.bio}
                Profile Images: ${externalProfile.imageDescriptions.joinToString("\n")}
                
                User Preferences:
                - Looking for: ${myOwnPrefs.lookingFor.joinToString(", ")}
                - Deal breakers: ${myOwnPrefs.dealBreakers.joinToString(", ")}
                - Interests: ${myOwnPrefs.interests.joinToString(", ")}
                - Age Range: ${myOwnPrefs.ageRange}
                - Location: ${myOwnPrefs.locationPreference}
                
                Provide a number between 0 and 1, where:
                0 = Not compatible at all (deal breakers present)
                1 = Highly compatible
            """.trimIndent()

            // Call Gemini API for analysis
            val response = callGeminiAPI(prompt)

            // Parse response to get compatibility score (0-1)
            val score = response.toDoubleOrNull() ?: 0.5
            return (score * 100).toInt() // Convert to 0-100 scale
        } catch (e: Exception) {
            when (e) {
                is FileNotFoundException -> println("Error: user_preferences.json not found")
                else -> println("Error reading or parsing preferences: ${e.message}")
            }
            throw e
        }
    }

    suspend fun extractProfile(imageBuffers: List<Buffer>): ExternalProfile {
        try {
            val imageDescriptions = mutableListOf<String>()

            for (imageBuffer in imageBuffers) {
                val prompt = """
                    Analyze this dating profile image. Describe:
                    1. The person's appearance
                    2. The setting/background
                    3. Any visible activities or interests
                    4. Relevant details for dating context
                    Be concise but thorough.
                """.trimIndent()

                // Call Gemini API for each image
                val copiedImageBuffer = imageBuffer.copy()
                val description = callGeminiAPI(copiedImageBuffer, prompt)
                imageDescriptions.add(description)
            }

            // Extract bio from the first image (assuming it contains profile text)
            val bioPrompt =
                "Extract any visible profile text or bio from this image. If none, describe the main person."
            val bio = callGeminiAPI(imageBuffers.firstOrNull(), bioPrompt)

            return ExternalProfile(
                bio = bio,
                imageDescriptions = imageDescriptions,
            )
        } catch (e: Exception) {
            println("Error extracting text from images: ${e.message}")
            throw e
        }
    }

    private suspend fun callGeminiAPI(prompt: String): String {
        val model = GenerativeModel(
            modelName = "gemini-2.0-flash",
            apiKey = apiKey,
        )

        val response = model.generateContent(prompt)
        return response.text?.trim() ?: throw IllegalStateException("No response from Gemini")
    }

    private suspend fun callGeminiAPI(image: Buffer?, prompt: String): String {
        val model = GenerativeModel(
            modelName = "gemini-2.0-flash",
            apiKey = apiKey,
        )

        val imageBytes = image?.readByteArray()

        // Create content with both image and text
        val content = content {
            if (imageBytes != null) {
                image(imageBytes)
            }
            text(prompt)
        }

        val response = model.generateContent(content)
        return response.text?.trim() ?: throw IllegalStateException("No response from Gemini")
    }

    private fun getImages() {
    }

    /**
     * Returns 1 if profile is to be swiped right, 0 otherwise.
     */
    suspend fun analyzeProfile(
        myOwnPrefs: MyOwnPreferences,
        images: List<Buffer>
    ): Int {
        val externalProfile = extractProfile(images)
        println("Extracted Text: $externalProfile")

        val decision = compareProfiles(
            myOwnPrefs = myOwnPrefs,
            externalProfile = externalProfile,
        )
        return decision
    }
}

/*
suspend fun main(args: Array<String>) {
    if (args.isEmpty()) {
        println("Please provide an image path as argument")
        return
    }

    try {
        val apiKey = System.getenv("GEMINI_API_KEY") ?: throw IllegalStateException("GEMINI_API_KEY not set")
        val gemini = Gemini(apiKey)
        val result = gemini.analyzeProfile(args[0])
        println(result)
    } catch (e: Exception) {
        println("Error in main: ${e.message}")
    }
}
*/

```