Repository: qappsabdul/storyteller
Pull request #11: Implement story generation from images
Author: login: amazon-q-developer[bot]
Created: 2025-05-06T14:44:50Z | Merged: 2025-05-06T14:48:26Z

This pull request implements a new image-to-story generation feature that allows users to upload images and receive AI-generated stories based on those images. The implementation includes:

- New API endpoint for image-to-story generation
- Integration with Claude AI via AWS Bedrock for story generation
- Image upload handling and processing
- Documentation for the new feature

The change adds necessary routes, controllers, and middleware for handling image uploads and story generation, along with comprehensive error handling and appropriate API responses.


Diff:
diff --git a/README-image-to-story.md b/README-image-to-story.md
new file mode 100644
index 0000000..681955f
--- /dev/null
+++ b/README-image-to-story.md
@@ -0,0 +1,54 @@
+# Image to Story Generation
+
+This feature allows users to upload images and receive AI-generated stories based on the content of those images. The implementation leverages Claude Sonnet 3.7 via AWS Bedrock to analyze images and create creative stories.
+
+## How to Use
+
+### Endpoint
+
+```
+POST /api/stories/generate-from-image
+```
+
+### Request Format
+
+Send a multipart/form-data request with an image file:
+
+```
+Key: image
+Value: [your-image-file]
+```
+
+### Response Format
+
+```json
+{
+  "success": true,
+  "message": "Story generated successfully",
+  "data": {
+    "story": "The AI-generated story text...",
+    "image": {
+      "filename": "uploaded-image-filename.jpg",
+      "path": "uploads/uploaded-image-filename.jpg"
+    }
+  }
+}
+```
+
+## Implementation Details
+
+1. The application receives an image upload through the endpoint
+2. The image is processed by the upload middleware and saved to the file system
+3. The image is read from the file system and converted to base64 format
+4. The base64 image is included in a prompt sent to Claude AI via AWS Bedrock
+5. Claude analyzes the image content and generates a creative story
+6. The story is returned to the client along with references to the uploaded image
+
+## Error Handling
+
+The endpoint includes error handling for various scenarios:
+- Missing image uploads
+- File processing errors
+- AI service errors
+
+Each error returns an appropriate status code and error message.
\ No newline at end of file
diff --git a/src/controllers/imageToStoryController.js b/src/controllers/imageToStoryController.js
new file mode 100644
index 0000000..7d83637
--- /dev/null
+++ b/src/controllers/imageToStoryController.js
@@ -0,0 +1,66 @@
+const fs = require('fs');
+const { generateWithClaude } = require('../services/bedrock');
+const path = require('path');
+
+/**
+ * Generate a story from an uploaded image
+ * @param {Object} req - Express request object
+ * @param {Object} res - Express response object
+ */
+const generateStoryFromImage = async (req, res) => {
+    try {
+        // Check if file was uploaded
+        if (!req.file) {
+            return res.status(400).json({
+                success: false,
+                message: 'No image uploaded'
+            });
+        }
+
+        // Get file path and read image
+        const filePath = req.file.path;
+        const imageBuffer = fs.readFileSync(filePath);
+        
+        // Convert image to base64
+        const base64Image = imageBuffer.toString('base64');
+        
+        // Prepare prompt for Claude
+        const prompt = `Generate a creative story based on this image. Be descriptive and imaginative about what you see in the image and craft a compelling narrative around it. The story should be approximately 300-500 words.
+
+[IMAGE: data:${req.file.mimetype};base64,${base64Image}]`;
+
+        // Options for Claude
+        const options = {
+            maxTokens: 2048, // Increased token limit for longer stories
+            temperature: 0.8, // Slightly higher temperature for creativity
+            topP: 0.9
+        };
+
+        // Generate story using Claude
+        const storyText = await generateWithClaude(prompt, options);
+        
+        // Return success response with story
+        res.status(200).json({
+            success: true,
+            message: 'Story generated successfully',
+            data: {
+                story: storyText,
+                image: {
+                    filename: req.file.filename,
+                    path: req.file.path
+                }
+            }
+        });
+
+    } catch (error) {
+        console.error('Error generating story from image:', error);
+        res.status(500).json({
+            success: false,
+            error: error.message || 'Failed to generate story from image'
+        });
+    }
+};
+
+module.exports = {
+    generateStoryFromImage
+};
\ No newline at end of file
diff --git a/src/routes/imageToStoryRoutes.js b/src/routes/imageToStoryRoutes.js
new file mode 100644
index 0000000..cc1b547
--- /dev/null
+++ b/src/routes/imageToStoryRoutes.js
@@ -0,0 +1,13 @@
+const express = require('express');
+const router = express.Router();
+const { generateStoryFromImage } = require('../controllers/imageToStoryController');
+const upload = require('../middleware/uploadMiddleware');
+
+/**
+ * @route   POST /api/stories/generate-from-image
+ * @desc    Generate a story from an uploaded image using Claude AI
+ * @access  Public
+ */
+router.post('/generate-from-image', upload.single('image'), generateStoryFromImage);
+
+module.exports = router;
\ No newline at end of file
diff --git a/src/routes/index.js b/src/routes/index.js
index 7de29f9..9a2379c 100644
--- a/src/routes/index.js
+++ b/src/routes/index.js
@@ -3,11 +3,13 @@ const router = express.Router();
 const sampleRoutes = require('./sampleRoutes');
 const uploadRoutes = require('./uploadRoutes');
 const claudeRoutes = require('./claudeRoutes');
+const imageToStoryRoutes = require('./imageToStoryRoutes');
 
 // Mount routes
 router.use('/api/samples', sampleRoutes);
 router.use('/api/uploads', uploadRoutes);
 router.use('/api/claude', claudeRoutes);
+router.use('/api/stories', imageToStoryRoutes);
 
 // Base route
 router.get('/', (req, res) => {


Reviews:
user: login: amazon-q-developer[bot]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T14:46:05Z
user: login: amazon-q-developer[bot]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T14:46:06Z
user: login: amazon-q-developer[bot]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T14:46:38Z
user: login: amazon-q-developer[bot]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T14:46:38Z

Review comments:
diff_hunk: @@ -0,0 +1,66 @@
+const fs = require('fs');
+const { generateWithClaude } = require('../services/bedrock');
+const path = require('path');
+
+/**
+ * Generate a story from an uploaded image
+ * @param {Object} req - Express request object
+ * @param {Object} res - Express response object
+ */
+const generateStoryFromImage = async (req, res) => {
+    try {
+        // Check if file was uploaded
+        if (!req.file) {
+            return res.status(400).json({
+                success: false,
+                message: 'No image uploaded'
+            });
+        }
+
+        // Get file path and read image
+        const filePath = req.file.path;
+        const imageBuffer = fs.readFileSync(filePath);
path: src/controllers/imageToStoryController.js
user: login: amazon-q-developer[bot]
body: > [!WARNING]
> ****Description:**** The code doesn't handle potential file system errors when reading the image file. Wrap the fs.readFileSync() call in a try-catch block to handle potential file system errors.

****Severity:**** High


created_at: 2025-05-06T14:46:05Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,13 @@
+const express = require('express');
+const router = express.Router();
+const { generateStoryFromImage } = require('../controllers/imageToStoryController');
+const upload = require('../middleware/uploadMiddleware');
+
+/**
+ * @route   POST /api/stories/generate-from-image
+ * @desc    Generate a story from an uploaded image using Claude AI
+ * @access  Public
+ */
+router.post('/generate-from-image', upload.single('image'), generateStoryFromImage);
path: src/routes/imageToStoryRoutes.js
user: login: amazon-q-developer[bot]
body: > [!WARNING]
> ****Description:**** Disabling cross-site request forgery (CSRF) protection can make your users vulnerable to attacks. Enable CSRF if your application can be accessed through a browser and another token mechanism isn't in place.

[Learn more](https://cwe.mitre.org/data/definitions/352.html)

****Severity:**** High


created_at: 2025-05-06T14:46:06Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,66 @@
+const fs = require('fs');
+const { generateWithClaude } = require('../services/bedrock');
+const path = require('path');
+
+/**
+ * Generate a story from an uploaded image
+ * @param {Object} req - Express request object
+ * @param {Object} res - Express response object
+ */
+const generateStoryFromImage = async (req, res) => {
+    try {
+        // Check if file was uploaded
+        if (!req.file) {
+            return res.status(400).json({
+                success: false,
+                message: 'No image uploaded'
+            });
+        }
+
+        // Get file path and read image
+        const filePath = req.file.path;
+        const imageBuffer = fs.readFileSync(filePath);
path: src/controllers/imageToStoryController.js
user: login: amazon-q-developer[bot]
body: The fix addresses the comment by wrapping the `fs.readFileSync()` call in a try-catch block to handle potential file system errors. This is important because file system operations can fail for various reasons, such as insufficient permissions, file not found, or disk errors.

Here's a detailed explanation of the fix:

1. We introduce a new `try-catch` block specifically for the file reading operation.
2. Inside this block, we attempt to read the file using `fs.readFileSync()`.
3. If an error occurs during file reading, it's caught in the `catch` block.
4. In the `catch` block, we log the error for debugging purposes and send an appropriate error response to the client.

This fix improves the robustness of the code by:
1. Preventing the application from crashing due to unhandled file system errors.
2. Providing a more informative error message to the client when file reading fails.
3. Allowing for proper error logging, which can help with debugging and monitoring.

Additional examples of error handling in file operations:

1. Using async/await with promises:
```javascript
const fs = require('fs').promises;

try {
  const data = await fs.readFile(filePath);
  // Process data
} catch (error) {
  console.error('Error reading file:', error);
  // Handle error
}
```

2. Using callbacks (older style):
```javascript
fs.readFile(filePath, (err, data) => {
  if (err) {
    console.error('Error reading file:', err);
    // Handle error
    return;
  }
  // Process data
});
```

References:
1. Node.js Documentation on File System: https://nodejs.org/api/fs.html
2. Error Handling in Node.js: https://nodejs.dev/learn/error-handling-in-nodejs

By implementing this fix, the code becomes more resilient to potential file system issues, providing a better user experience and easier maintenance for developers.
```suggestion
// Get file path and read image
        const filePath = req.file.path;
        let imageBuffer;
        try {
            imageBuffer = fs.readFileSync(filePath);
        } catch (fsError) {
            console.error('Error reading image file:', fsError);
            return res.status(500).json({
                success: false,
                message: 'Error reading uploaded image'
            });
        }
        
        // Convert image to base64
        const base64Image = imageBuffer.toString('base64');
```

created_at: 2025-05-06T14:46:38Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,13 @@
+const express = require('express');
+const router = express.Router();
+const { generateStoryFromImage } = require('../controllers/imageToStoryController');
+const upload = require('../middleware/uploadMiddleware');
+
+/**
+ * @route   POST /api/stories/generate-from-image
+ * @desc    Generate a story from an uploaded image using Claude AI
+ * @access  Public
+ */
+router.post('/generate-from-image', upload.single('image'), generateStoryFromImage);
path: src/routes/imageToStoryRoutes.js
user: login: amazon-q-developer[bot]
body: Vulnerability: Cross-Site Request Forgery (CSRF)

The original code was vulnerable to CSRF attacks because it lacked proper protection mechanisms. CSRF attacks occur when malicious websites trick authenticated users into performing unwanted actions on a different site where they're logged in.

Remediation:
1. We imported the 'cookie-parser' and 'csurf' packages.
2. We initialized CSRF protection using the csrf() function with cookie-based tokens.
3. We added the csrfProtection middleware to the route.

This fix works by:
1. Using cookies to store CSRF tokens (enabled by cookie-parser).
2. Generating and validating CSRF tokens for each request (handled by csurf).
3. Applying CSRF protection to the specific route.

By implementing these changes, we ensure that only legitimate requests from our application's forms are processed, preventing unauthorized actions triggered by malicious sites.

References:
1. OWASP CSRF Prevention Cheat Sheet: https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html
2. Express.js csurf middleware: https://github.com/expressjs/csurf

Other examples of CSRF protection:
1. Using custom headers (e.g., X-CSRF-Token)
2. Double submit cookie pattern
3. Synchronizer token pattern

It's important to note that while this fix addresses CSRF, a comprehensive security approach should also consider other potential vulnerabilities like XSS, SQL injection, and proper input validation.
```suggestion
// Import required packages
const cookieParser = require('cookie-parser'); // Used for parsing cookies
const csrf = require('csurf'); // Used for CSRF protection

// Initialize CSRF protection
const csrfProtection = csrf({ cookie: true });

router.post('/generate-from-image', csrfProtection, upload.single('image'), generateStoryFromImage);
```

created_at: 2025-05-06T14:46:38Z
author_association: CONTRIBUTOR

PR comments:
user: login: amazon-q-developer[bot]
created_at: 2025-05-06T14:44:52Z
body: Resolves #4
author_association: CONTRIBUTOR
user: login: amazon-q-developer[bot]
created_at: 2025-05-06T14:44:53Z
body: To provide feedback, navigate to the **Files changed** tab and leave comments on the proposed code changes. Choose **Start review** for each comment, and then choose **Request changes**, and I'll propose revised changes.
author_association: CONTRIBUTOR
user: login: amazon-q-developer[bot]
created_at: 2025-05-06T14:44:54Z
body: :hourglass_flowing_sand: I'm reviewing this pull request for security vulnerabilities and code quality issues. I'll provide an update when I'm done
author_association: CONTRIBUTOR
user: login: amazon-q-developer[bot]
created_at: 2025-05-06T14:46:07Z
body: :white_check_mark: I finished the code review, and left comments with the issues I found. I will now generate code fix suggestions.
author_association: CONTRIBUTOR