diff --git a/CONSTITUTION.md b/CONSTITUTION.md new file mode 100644 index 0000000..e69de29 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..e69de29 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..e69de29 diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..e69de29 diff --git a/README.md b/README.md new file mode 100644 index 0000000..e1783dd --- /dev/null +++ b/README.md @@ -0,0 +1,39 @@ +# SpeakEasy: Know who's talking + +## Client flow +1. Client uploads `EPUB` +2. Backend: + - Stores file + - Creates `book_id` + - Publishes job to RabbitMQ +3. Client subscribes: + - `GET /books/{book_id}/stream` via WebSocket +4. Backend: + - Authenticates user + - Remembers: `connection -> book_id` +5. Worker processes text: + - Calls Ollama + - Stores result in Postgres + - Emits "result ready" event +6. Backend: + - Receives event + - Pushes mathing results to the subscribed client + +### Example WebSocket message to client +```json +{ + "book_id": "book-abc", + "paragraph_id": "p-432", + "speaker": "Mary", + "confidence": 0.87 +} +``` + +## Worker flow +1. Receives job from RabbitMQ +2. Loads paragraph text +3. Embeds paragraph +4. Queries vector DB (pgvector) +5. Builds prompt with retrieved content +6. Sends prompt to Ollama +7. Stores result diff --git a/ai/AI.md b/ai/AI.md new file mode 100644 index 0000000..81b54e0 --- /dev/null +++ b/ai/AI.md @@ -0,0 +1,18 @@ +# AI + +## FILES +- **client** `AI Service Clients` + - client.go `Client Interface` + - ollama.go `Ollama HTTP Client` +- **detector** `Speaker Detection Logic` + - context.go `Context tracking for conversations` + - patterns.go `Rule-based Fallback Detection` + - speaker.go `Main Speaker Detection` +- **models** `Model Management` + - config.go `Model Configurations` + - manager.go `Model Downloading/Switching` + - registry.go `Available Models Registry` +- **prompts** `Prompt Templates` + - speaker_detection.go `Prompts for Speaker Identification` + - templates.go `Prompt Template Utilities` + diff --git a/ai/client/client.go b/ai/client/client.go new file mode 100644 index 0000000..b1a4ed5 --- /dev/null +++ b/ai/client/client.go @@ -0,0 +1,2 @@ +package client + diff --git a/ai/client/ollama.go b/ai/client/ollama.go new file mode 100644 index 0000000..b1a4ed5 --- /dev/null +++ b/ai/client/ollama.go @@ -0,0 +1,2 @@ +package client + diff --git a/ai/detector/context.go b/ai/detector/context.go new file mode 100644 index 0000000..ec8ea9b --- /dev/null +++ b/ai/detector/context.go @@ -0,0 +1,2 @@ +package detector + diff --git a/ai/detector/patterns.go b/ai/detector/patterns.go new file mode 100644 index 0000000..ec8ea9b --- /dev/null +++ b/ai/detector/patterns.go @@ -0,0 +1,2 @@ +package detector + diff --git a/ai/detector/speaker.go b/ai/detector/speaker.go new file mode 100644 index 0000000..ec8ea9b --- /dev/null +++ b/ai/detector/speaker.go @@ -0,0 +1,2 @@ +package detector + diff --git a/ai/models/config.go b/ai/models/config.go new file mode 100644 index 0000000..d6a05a4 --- /dev/null +++ b/ai/models/config.go @@ -0,0 +1,2 @@ +package models + diff --git a/ai/models/manager.go b/ai/models/manager.go new file mode 100644 index 0000000..d6a05a4 --- /dev/null +++ b/ai/models/manager.go @@ -0,0 +1,2 @@ +package models + diff --git a/ai/models/registry.go b/ai/models/registry.go new file mode 100644 index 0000000..d6a05a4 --- /dev/null +++ b/ai/models/registry.go @@ -0,0 +1,2 @@ +package models + diff --git a/ai/prompts/speaker_detection.go b/ai/prompts/speaker_detection.go new file mode 100644 index 0000000..f5d230a --- /dev/null +++ b/ai/prompts/speaker_detection.go @@ -0,0 +1,2 @@ +package prompts + diff --git a/ai/prompts/templates.go b/ai/prompts/templates.go new file mode 100644 index 0000000..f5d230a --- /dev/null +++ b/ai/prompts/templates.go @@ -0,0 +1,2 @@ +package prompts + diff --git a/app.go b/app.go new file mode 100644 index 0000000..9d9d3e8 --- /dev/null +++ b/app.go @@ -0,0 +1,10 @@ +package main + +import server_config "speakeasy/config/server" + +func main() { + sc := server_config.CreateNewConfig() + sc.LoadConfig() + println(sc.SERVER_HOST) + println(sc.SERVER_PORT) +} diff --git a/client/CLIENT.md b/client/CLIENT.md new file mode 100644 index 0000000..e69de29 diff --git a/config/CONFIG.md b/config/CONFIG.md new file mode 100644 index 0000000..d10b87a --- /dev/null +++ b/config/CONFIG.md @@ -0,0 +1,8 @@ +# CONFIG + +- **server** `Server Configuration` + - config.go + - env.go +- **frontend** `Client Configuration` + - config.go + - env.go diff --git a/config/frontend/config.go b/config/frontend/config.go new file mode 100644 index 0000000..d698e76 --- /dev/null +++ b/config/frontend/config.go @@ -0,0 +1 @@ +package frontend diff --git a/config/frontend/env.go b/config/frontend/env.go new file mode 100644 index 0000000..d698e76 --- /dev/null +++ b/config/frontend/env.go @@ -0,0 +1 @@ +package frontend diff --git a/config/server/config.go b/config/server/config.go new file mode 100644 index 0000000..a523f85 --- /dev/null +++ b/config/server/config.go @@ -0,0 +1,62 @@ +package server + +import ( + // "log" + "speakeasy/utils/env" + "time" +) + +var ( + VERSION = "dev" + BUILD_TIME = time.Now() +) + +// ServerConfig is a configuration model for the backend +type ServerConfig struct { + PG_HOST string + PG_USER string + PG_PASSWORD string + RABBITMQ_HOST string + RABBITMQ_USER string + RABBITMQ_PASSWORD string + OLLAMA_HOST string + OLLAMA_MODEL_ACCURACY string + SERVER_HOST string + SERVER_PORT int + CLIENT_HOST string + CLIENT_PORT int + VERSION string + BUILD_TIME time.Time +} + +// ModelAccuracy is a configuration option for the Ollama Model Manager +type ModelAccuracy string + +const ( + FAST ModelAccuracy = "FAST" // gemma:2b + BALANCED ModelAccuracy = "BALANCED" // phi3:mini + ACCURATE ModelAccuracy = "ACCURATE" // llama3.1:8b +) + +// CreateNewConfig returns an empty server config +func CreateNewConfig() ServerConfig { + return ServerConfig{} +} + +// LoadConfig loads config from environment variables +func (c *ServerConfig) LoadConfig() { + c.PG_HOST = env.GetString("PG_HOST", "localhost:8888") + c.PG_USER = env.GetString("PG_USER", "speakeasy") + c.PG_PASSWORD = env.GetString("PG_PASSWORD", "speakeasy") + c.RABBITMQ_HOST = env.GetString("RABBITMQ_HOST", "localhost:5672") + c.RABBITMQ_PASSWORD = env.GetString("RABBITMQ_PASSWORD", "speakeasy") + c.RABBITMQ_USER = env.GetString("RABBITMQ_USER", "speakeasy") + c.OLLAMA_HOST = env.GetString("OLLAMA_HOST", "localhost:11434") + c.OLLAMA_MODEL_ACCURACY = env.GetString("OLLAMA_MODEL_ACCURACY", "BALANCED") + c.SERVER_HOST = env.GetString("SERVER_HOST", "localhost") + c.SERVER_PORT = env.GetInt("SERVER_PORT", 3456) + c.CLIENT_HOST = env.GetString("CLIENT_HOST", "localhost") + c.CLIENT_PORT = env.GetInt("CLIENT_PORT", 3457) + c.VERSION = VERSION + c.BUILD_TIME = BUILD_TIME +} diff --git a/config/server/env.go b/config/server/env.go new file mode 100644 index 0000000..abb4e43 --- /dev/null +++ b/config/server/env.go @@ -0,0 +1 @@ +package server diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..cb10403 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,54 @@ +services: + # Postgres with pgvector extension + db: + container_name: speakeasy_db + image: pgvector/pgvector:pg18 + restart: unless-stopped + environment: + POSTGRES_USER: speakeasy + POSTGRES_PASSWORD: speakeasy + POSTGRES_DB: speakeasy + ports: + - "8888:5432" + volumes: + - pg_data:/var/lib/postgres + - ./init:/docker-entrypoint-initdb.d + networks: + - speakeasy_network + + # RabbitMQ server + rabbitmq: + container_name: speakeasy_rabbitmq + image: rabbitmq:4.2.2-management-alpine + restart: unless-stopped + ports: + - "5672:5672" # Default port + - "15672:15672" # Management port + environment: + RABBITMQ_DEFAULT_USER: speakeasy + RABBITMQ_DEFAULT_PASS: speakeasy + volumes: + - rabbitmq_data:/var/lib/rabbitmq + networks: + - speakeasy_network + + # Ollama server + ollama: + container_name: speakeasy_ollama + image: ollama/ollama:latest + restart: unless-stopped + ports: + - "11434:11434" + networks: + - speakeasy_network + volumes: + - ollama_data:/root/.ollama + + +volumes: + pg_data: + rabbitmq_data: + ollama_data: + +networks: + speakeasy_network: diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..dda8475 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module speakeasy + +go 1.25.5 diff --git a/init/init_pgvector.sql b/init/init_pgvector.sql new file mode 100644 index 0000000..0aa0fc2 --- /dev/null +++ b/init/init_pgvector.sql @@ -0,0 +1 @@ +CREATE EXTENSION IF NOT EXISTS vector; diff --git a/models/MODELS.md b/models/MODELS.md new file mode 100644 index 0000000..2361398 --- /dev/null +++ b/models/MODELS.md @@ -0,0 +1,9 @@ +# MODELS + +## FILES +- **book** `Interfaces for Elements of the Book` + - model.go `Structs` + - functions.go `Helper Functions` +- **citations** `Interfaces for Interacting With Citations` + - model.go `Structs` + - functions.go `Helper Functions` diff --git a/models/book/functions.go b/models/book/functions.go new file mode 100644 index 0000000..9b3375d --- /dev/null +++ b/models/book/functions.go @@ -0,0 +1,8 @@ +package book + +import "fmt" + +// String helper method to convert dialogue struct into a string +func (d *Dialogue) String() string { + return fmt.Sprintf("%s", d.Content) +} diff --git a/models/book/model.go b/models/book/model.go new file mode 100644 index 0000000..1ae18d2 --- /dev/null +++ b/models/book/model.go @@ -0,0 +1,43 @@ +package book + +// Book is a model for storing chapters, author, title, and total pages +type Book struct { + Title string `json:"title"` + Chapters []Chapter `json:"chapters"` + TotalPages int `json:"total_pages"` +} + +// Chapter is a model that holds chaper number and pages within that chapter +type Chapter struct { + Index int `json:"index"` // Chapter number + Pages []Page `json:"pages"` +} + +// Page is model that stores page number and paragraphs +type Page struct { + Index int `json:"index"` + Paragraphs []Paragraph `json:"paragraphs"` +} + +// Paragraph is a model that stores paragraph number and lines +type Paragraph struct { + Index int `json:"index"` + Lines []Line `json:"lines"` +} + +// Line is an interface for storing the content of a line +type Line struct { + Index int `json:"index"` + Content any `json:"content"` +} + +// Dialogue is a model that stores the speaker, and text +type Dialogue struct { + Speaker *string `json:"speaker,omitempty"` + Content string `json:"content"` +} + +// Narration is a model that stores text that isnt dialogue +type Narration struct { + Content string `json:"content"` + diff --git a/models/citations/functions.go b/models/citations/functions.go new file mode 100644 index 0000000..e557a00 --- /dev/null +++ b/models/citations/functions.go @@ -0,0 +1,13 @@ +package citations + +import "fmt" + +// Method to return a MLA citation as a string +func (c *MLACitation) String() string { + return fmt.Sprintf(`"%s" (%s, %d)`, c.Content, c.LastName, c.PageNumber) +} + +// Method to return a APA citation as a string +func (c *APACitation) String() string { + return fmt.Sprintf(`"%s" (%s, %d, p. %d)`, c.Content, c.LastName, c.Year, c.PageNumber) +} diff --git a/models/citations/model.go b/models/citations/model.go new file mode 100644 index 0000000..4e64b04 --- /dev/null +++ b/models/citations/model.go @@ -0,0 +1,17 @@ +package citations + +// MLACitation is a model for creating an inline citation in the MLA format: (Doe 23) +type MLACitation struct { + Content string `json:"content"` + LastName string `json:"last_name"` + PageNumber int `json:"page_number"` +} + +// APACitation is a model for creating an inline citation in the APA format: (Doe, 2020, p. 23) +type APACitation struct { + Content string `json:"content"` + LastName string `json:"last_name"` + Year int `json:"year"` + PageNumber int `json:"page_number"` +} + diff --git a/parser/PARSER.md b/parser/PARSER.md new file mode 100644 index 0000000..e69de29 diff --git a/rabbitmq/RABBITMQ.md b/rabbitmq/RABBITMQ.md new file mode 100644 index 0000000..e69de29 diff --git a/rag/RAG.md b/rag/RAG.md new file mode 100644 index 0000000..e69de29 diff --git a/speakeasy b/speakeasy new file mode 100755 index 0000000..229c40e Binary files /dev/null and b/speakeasy differ diff --git a/ui/UI.md b/ui/UI.md new file mode 100644 index 0000000..e69de29 diff --git a/utils/UTILS.md b/utils/UTILS.md new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/utils/UTILS.md @@ -0,0 +1 @@ + diff --git a/utils/env/loader.go b/utils/env/loader.go new file mode 100644 index 0000000..2611d20 --- /dev/null +++ b/utils/env/loader.go @@ -0,0 +1,41 @@ +package env + +import ( + "os" + "strconv" +) + +// GetStringOrNull retrieves a string environment variable or returns the default value if not set +func GetString(name string, defaultValue string) string { + value := os.Getenv(name) + if value == "" { + return defaultValue + } + return value +} + +// GetIntOrNull retrieves an int environment variable or returns the default value if not set +func GetInt(name string, defaultValue int) int { + value := os.Getenv(name) + if value == "" { + return defaultValue + } + intValue, err := strconv.Atoi(value) + if err != nil { + return defaultValue + } + return intValue +} + +// GetBoolOrNull retrieves a bool environment variable or returns the default value if not set +func GetBool(name string, defaultValue bool) bool { + value := os.Getenv(name) + if value == "" { + return defaultValue + } + boolValue, err := strconv.ParseBool(value) + if err != nil { + return defaultValue + } + return boolValue +}