Pet Story Generator Tutorial for Beginners
July 25, 2025 · View on GitHub
Table of Contents
- Prerequisites
- Understanding the Project Structure
- Core Components Explained
- Running the Application
- How It All Works Together
- Understanding the AI Integration
- Next Steps
Prerequisites
Before starting, make sure you have:
- Java 21 or higher installed
- Maven for dependency management
- A GitHub account with a personal access token (PAT) with
models:readscope - Basic understanding of Java, Spring Boot, and web development
Understanding the Project Structure
The pet story project has several important files:
petstory/
├── src/main/java/com/example/petstory/
│ ├── PetStoryApplication.java # Main Spring Boot application
│ ├── PetController.java # Web request handler
│ ├── StoryService.java # AI story generation service
│ └── SecurityConfig.java # Security configuration
├── src/main/resources/
│ ├── application.properties # App configuration
│ └── templates/
│ ├── index.html # Upload form page
│ └── result.html # Story display page
└── pom.xml # Maven dependencies
Core Components Explained
1. Main Application
File: PetStoryApplication.java
This is the entry point for our Spring Boot application:
@SpringBootApplication
public class PetStoryApplication {
public static void main(String[] args) {
SpringApplication.run(PetStoryApplication.class, args);
}
}
What this does:
@SpringBootApplicationannotation enables auto-configuration and component scanning- Starts an embedded web server (Tomcat) on port 8080
- Creates all necessary Spring beans and services automatically
2. Web Controller
File: PetController.java
This handles all web requests and user interactions:
@Controller
public class PetController {
private final StoryService storyService;
public PetController(StoryService storyService) {
this.storyService = storyService;
}
@GetMapping("/")
public String index() {
return "index"; // Returns index.html template
}
@PostMapping("/generate-story")
public String generateStory(@RequestParam("description") String description,
Model model,
RedirectAttributes redirectAttributes) {
// Input validation
if (description.trim().isEmpty()) {
redirectAttributes.addFlashAttribute("error", "Please provide a description.");
return "redirect:/";
}
// Sanitize input for security
String sanitizedDescription = sanitizeInput(description);
// Generate story with error handling
try {
String story = storyService.generateStory(sanitizedDescription);
model.addAttribute("caption", sanitizedDescription);
model.addAttribute("story", story);
return "result"; // Returns result.html template
} catch (Exception e) {
// Use fallback story if AI fails
String fallbackStory = generateFallbackStory(sanitizedDescription);
model.addAttribute("story", fallbackStory);
return "result";
}
}
private String sanitizeInput(String input) {
return input.replaceAll("[<>\"'&]", "") // Remove dangerous characters
.trim()
.substring(0, Math.min(input.length(), 500)); // Limit length
}
}
Key features:
- Route Handling:
@GetMapping("/")shows the upload form,@PostMapping("/generate-story")processes submissions - Input Validation: Checks for empty descriptions and length limits
- Security: Sanitizes user input to prevent XSS attacks
- Error Handling: Provides fallback stories when AI service fails
- Model Binding: Passes data to HTML templates using Spring's
Model
Fallback System: The controller includes pre-written story templates that are used when the AI service is unavailable:
private String generateFallbackStory(String description) {
String[] storyTemplates = {
"Meet the most wonderful pet in the world – a furry ball of energy...",
"Once upon a time, there lived a remarkable pet whose heart was as big...",
"In a cozy home filled with love, there lived an extraordinary pet..."
};
// Use description hash for consistent responses
int index = Math.abs(description.hashCode() % storyTemplates.length);
return storyTemplates[index];
}
3. Story Service
File: StoryService.java
This service communicates with GitHub Models to generate stories:
@Service
public class StoryService {
private final OpenAIClient openAIClient;
private final String modelName;
public StoryService(@Value("${github.models.endpoint}") String endpoint,
@Value("${github.models.model}") String modelName) {
String githubToken = System.getenv("GITHUB_TOKEN");
if (githubToken == null || githubToken.isBlank()) {
throw new IllegalStateException("GITHUB_TOKEN environment variable must be set");
}
// Create OpenAI client configured for GitHub Models
this.openAIClient = OpenAIOkHttpClient.builder()
.baseUrl(endpoint)
.apiKey(githubToken)
.build();
}
public String generateStory(String description) {
String systemPrompt = "You are a creative storyteller who writes fun, " +
"family-friendly short stories about pets. " +
"Keep stories under 500 words and appropriate for all ages.";
String userPrompt = "Write a fun short story about a pet described as: " + description;
// Configure the AI request
ChatCompletionCreateParams params = ChatCompletionCreateParams.builder()
.model(modelName)
.addSystemMessage(systemPrompt)
.addUserMessage(userPrompt)
.maxCompletionTokens(500) // Limit response length
.temperature(0.8) // Control creativity (0.0-1.0)
.build();
// Send request and get response
ChatCompletion response = openAIClient.chat().completions().create(params);
return response.choices().get(0).message().content().orElse("");
}
}
Key components:
- OpenAI Client: Uses the official OpenAI Java SDK configured for GitHub Models
- System Prompt: Sets the AI's behavior to write family-friendly pet stories
- User Prompt: Tells the AI exactly what story to write based on the description
- Parameters: Controls story length and creativity level
- Error Handling: Throws exceptions that the controller catches and handles
4. Web Templates
File: index.html (Upload Form)
The main page where users describe their pets:
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>Pet Story Generator</title>
<!-- CSS styling -->
</head>
<body>
<div class="container">
<h1>Pet Story Generator</h1>
<p>Describe your pet and we'll create a fun story about them!</p>
<!-- Error message display -->
<div th:if="${error}" class="error" th:text="${error}"></div>
<!-- Story generation form -->
<form action="/generate-story" method="post">
<div class="form-group">
<label for="description">Describe your pet:</label>
<textarea id="description" name="description"
placeholder="Tell us about your pet - what they look like, their personality, favorite activities..."
maxlength="1000" required></textarea>
</div>
<button type="submit" class="btn btn-primary">Generate Story</button>
</form>
<!-- Image upload section with client-side processing -->
<div class="upload-section">
<h2>Or Upload a Photo</h2>
<input type="file" id="imageInput" accept="image/*" />
<button onclick="analyzeImage()" class="upload-btn">Analyze Image</button>
</div>
<script>
// Client-side image analysis using Transformers.js
async function analyzeImage() {
// Image processing code here
// Generates description automatically from uploaded image
}
</script>
</div>
</body>
</html>
File: result.html (Story Display)
Shows the generated story:
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>Pet Story Result</title>
</head>
<body>
<div class="container">
<h1>Your Pet's Story</h1>
<div class="result-section">
<div class="result-label">Pet Description:</div>
<div class="result-content" th:text="${caption}"></div>
</div>
<div class="result-section">
<div class="result-label">Generated Story:</div>
<div class="result-content" th:text="${story}"></div>
</div>
<div class="result-section" th:if="${analysisType}">
<div class="result-label">Analysis Type:</div>
<div class="result-content" th:text="${analysisType}"></div>
</div>
<a href="/" class="back-link">Generate Another Story</a>
</div>
</body>
</html>
Template features:
- Thymeleaf Integration: Uses
th:attributes for dynamic content - Responsive Design: CSS styling for mobile and desktop
- Error Handling: Displays validation errors to users
- Client-side Processing: JavaScript for image analysis (using Transformers.js)
5. Configuration
File: application.properties
Configuration settings for the application:
spring.application.name=pet-story-app
# File upload limits
spring.servlet.multipart.max-file-size=10MB
spring.servlet.multipart.max-request-size=10MB
# Logging configuration
logging.level.com.example.petstory=INFO
# GitHub Models configuration
github.models.endpoint=https://models.github.ai/inference
github.models.model=openai/gpt-4.1-nano
Configuration explained:
- File Upload: Allows images up to 10MB
- Logging: Controls what information is logged during execution
- GitHub Models: Specifies which AI model and endpoint to use
- Security: Error handling configuration to avoid exposing sensitive information
Running the Application
Step 1: Set Your GitHub Token
First, you need to set your GitHub token as an environment variable:
Windows (Command Prompt):
set GITHUB_TOKEN=your_github_token_here
Windows (PowerShell):
$env:GITHUB_TOKEN="your_github_token_here"
Linux/macOS:
export GITHUB_TOKEN=your_github_token_here
Why this is needed:
- GitHub Models requires authentication to access AI models
- Using environment variables keeps sensitive tokens out of source code
- The
models:readscope provides access to AI inference
Step 2: Build and Run
Navigate to the project directory:
cd 04-PracticalSamples/petstory
Build the application:
mvn clean compile
Start the server:
mvn spring-boot:run
The application will start on http://localhost:8080.
Step 3: Test the Application
- Open
http://localhost:8080in your browser - Describe your pet in the text area (e.g., "A playful golden retriever who loves to fetch")
- Click "Generate Story" to get an AI-generated story
- Alternatively, upload a pet image to automatically generate a description
- View the creative story based on your pet's description
How It All Works Together
Here's the complete flow when you generate a pet story:
- User Input: You describe your pet on the web form
- Form Submission: Browser sends POST request to
/generate-story - Controller Processing:
PetControllervalidates and sanitizes the input - AI Service Call:
StoryServicesends request to GitHub Models API - Story Generation: AI generates a creative story based on the description
- Response Handling: Controller receives the story and adds it to the model
- Template Rendering: Thymeleaf renders
result.htmlwith the story - Display: User sees the generated story in their browser
Error Handling Flow: If the AI service fails:
- Controller catches the exception
- Generates a fallback story using pre-written templates
- Displays the fallback story with a note about AI unavailability
- User still gets a story, ensuring good user experience
Understanding the AI Integration
GitHub Models API
The application uses GitHub Models, which provides free access to various AI models:
// Authentication with GitHub token
this.openAIClient = OpenAIOkHttpClient.builder()
.baseUrl("https://models.github.ai/inference")
.apiKey(githubToken)
.build();
Prompt Engineering
The service uses carefully crafted prompts to get good results:
String systemPrompt = "You are a creative storyteller who writes fun, " +
"family-friendly short stories about pets. " +
"Keep stories under 500 words and appropriate for all ages.";
Response Processing
The AI response is extracted and validated:
ChatCompletion response = openAIClient.chat().completions().create(params);
String story = response.choices().get(0).message().content().orElse("");
Next Steps
For more examples, see Chapter 04: Practical samples