Inspiration

I've always been fascinated by the dark corners of history—the haunted places, the unsolved mysteries, the chilling "what ifs" that send shivers down your spine. I wanted to create an experience where anyone could explore a location's darkest secrets through AI-generated horror stories. The idea was to combine storytelling with real geographic data, so the terror feels personal and grounded in actual places. When Kiro IDE launched, I saw an opportunity to build something ambitious using its spec-driven development approach.

What it does

Hauntify is an AI-powered horror storytelling app that transforms simple prompts into immersive experiences:

  • Generates cinematic horror stories using Groq's LLaMA 3.3 70B model, streamed token-by-token in real-time
  • Narrates stories with professional AI voice synthesis via ElevenLabs (with Web Speech API fallback)
  • Visualizes locations on an interactive Leaflet map with animated orange-glow markers as each place is mentioned
  • Extracts historical timeline events that appear as vertical cards alongside the story
  • Synchronizes everything in real-time—streaming text, voice generation, and map updates happen simultaneously

The interface features a split-screen layout (chat on left, map on right) with a dark Halloween-themed aesthetic using black, orange, and purple colors.

How we built it

Tech Stack:

  • Frontend: Next.js 16 with React 19 and Tailwind CSS v4
  • AI Generation: Groq API (LLaMA 3.3 70B Versatile) for streaming story generation via Server-Sent Events
  • Voice Synthesis: ElevenLabs TTS API with Brian voice for dramatic narration
  • Maps: Leaflet with custom animated markers and polyline paths connecting events
  • Geocoding: Nominatim API (OpenStreetMap) with $1 \text{ req/sec}$ rate limiting and 30-day caching
  • State Management: Zustand with localStorage persistence for session recovery
  • Validation: Zod schemas for runtime type safety

Key Components:

  • useChatStream hook orchestrates the streaming pipeline from prompt to response
  • AudioQueueManager class handles sequential audio playback with proper blob URL lifecycle management
  • Server-side parser extracts ##TIMELINE## markers with JSON data (year, title, description, place)
  • Custom hooks (useAudioPlayer, useMapSync) coordinate voice playback and map updates

Challenges we ran into

Audio Queue Management: Building a custom AudioQueueManager that handles blob URLs, sequential playback, auto-advance, seek controls, and proper cleanup without memory leaks was complex. I implemented a listener pattern for state updates and careful URL.revokeObjectURL lifecycle management.

Streaming Pipeline Coordination: Orchestrating three simultaneous streams (text tokens, timeline extraction, voice generation) without blocking or race conditions required careful use of NDJSON event types and proper AbortControllers.

Timeline Extraction Accuracy: Parsing ##TIMELINE## markers from streaming text while handling malformed JSON, duplicate events, and invalid coordinates needed a robust parser with Zod validation and deduplication.

Rate Limiting for Geocoding: Nominatim API requires $1$ request per second. I implemented throttling with a 30-day localStorage cache to stay within limits while keeping the experience responsive.

Mobile Responsiveness: Creating a seamless experience across desktop ($60/40$ split), tablet ($50/50$), and mobile (tabbed interface) while keeping both components mounted to maintain state.

Accomplishments that we're proud of

  • A zero-latency streaming experience where text, voice, and map updates flow simultaneously as the story generates
  • A cinematic audio player with full controls (play/pause, seek, volume) and automatic queue management
  • A dark horror aesthetic that feels immersive with animated gradients, glowing markers, and Halloween-themed UI
  • Session persistence that restores full conversation history, timeline, audio queue, and map state on page reload
  • A robust fallback system—Web Speech API works when ElevenLabs isn't available, coordinate validation prevents map errors

What we learned

  • Streaming UX beats batch responses: Users perceive speed when something happens immediately (first token, voice starting, map marker appearing)
  • Audio blob lifecycle matters: Proper cleanup with URL.revokeObjectURL prevents memory leaks in long sessions
  • Rate limiting is essential: Respecting API limits with caching ensures reliability without getting blocked
  • Spec-driven development accelerates building: Writing requirements and design docs upfront with Kiro eliminated decision paralysis and prevented costly refactoring
  • Type safety catches bugs early: Zod schemas and TypeScript strict mode prevented countless runtime errors

What's next for Hauntify

  • Save & Share Stories with unique URLs and social media previews
  • Multi-Voice Cast with different voices for narrator vs. character dialogue
  • Enhanced Map Layers with historical overlays and custom horror-themed marker icons
  • Branching Narratives where users can influence the story direction
  • Ambient Audio Effects like reverb, echo, and background soundscapes for deeper immersion
  • Export Functionality to download stories as audio files or PDFs

Built With

  • elevenlabs-api
  • groq
  • groq-api-(llama-3.3-70b)
  • kiro
  • leaflet.js
  • motion
  • next.js-16
  • openstreetmap-nominatim
  • radix-ui
  • react-19
  • tailwind-css-4
  • typescript
  • vercel
  • zod
  • zustand
Share this project:

Updates