Files
bookshelf/books_flutter/lib/services/openai_service.dart
2026-02-08 12:05:05 +06:00

153 lines
4.6 KiB
Dart

import 'dart:io';
import 'dart:convert';
import 'package:books_flutter/config/api_config.dart';
import 'package:http/http.dart' as http;
import '../models/models.dart';
class OpenAIService {
final String apiKey;
final String baseUrl;
final String model;
late final String _endpoint;
OpenAIService({
required this.apiKey,
this.baseUrl = ApiConfig.openaiApiKey,
this.model = ApiConfig.openaiModel,
}) {
_endpoint = '$baseUrl/v1/chat/completions';
}
Future<Book?> analyzeBookCover(String imagePath) async {
try {
// Read the image file
final imageFile = File(imagePath);
final imageBytes = await imageFile.readAsBytes();
final base64Image = base64Encode(imageBytes);
// Create the prompt for book analysis
const prompt = '''
Analyze this book cover image and extract the following information in JSON format:
{
"title": "book title (required)",
"author": "author name (required)",
"genre": "fiction/fantasy/science/detective/biography/other",
"annotation": "brief description or summary if visible, otherwise generate a generic one"
}
Rules:
- Extract exact text from the cover
- If genre is unclear, choose the most appropriate one
- If annotation is not visible, create a brief generic description
- Return ONLY valid JSON, no additional text
- Ensure all required fields are present
- Return result in russian language
''';
// Create the request body for OpenAI API
final requestBody = {
'model': model, // Use the configured model
'messages': [
{
'role': 'user',
'content': [
{'type': 'text', 'text': prompt},
{
'type': 'image_url',
'image_url': {'url': 'data:image/jpeg;base64,$base64Image'},
},
],
},
],
};
// Make the API request
final response = await http.post(
Uri.parse(_endpoint),
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer $apiKey',
},
body: json.encode(requestBody),
);
if (response.statusCode != 200) {
print('OpenAI API error: ${response.statusCode}');
print('Response body: ${response.body}');
return null;
}
final responseData = json.decode(response.body);
// Extract the message content
final responseText = responseData['choices']?[0]?['message']?['content']
?.toString()
.trim();
if (responseText == null || responseText.isEmpty) {
print('Empty response from OpenAI');
return null;
}
// Extract JSON from response (handle potential markdown formatting)
String jsonString = responseText;
if (jsonString.contains('```json')) {
jsonString = jsonString.split('```json')[1].split('```')[0].trim();
} else if (jsonString.contains('```')) {
jsonString = jsonString.split('```')[1].split('```')[0].trim();
}
// Parse JSON response
final Map<String, dynamic> jsonData = json.decode(jsonString);
// Create Book object with extracted data
final Book book = (
id: DateTime.now().millisecondsSinceEpoch.toString(),
title: jsonData['title']?.toString() ?? 'Неизвестная книга',
author: jsonData['author']?.toString() ?? 'Неизвестный автор',
genre: _normalizeGenre(jsonData['genre']?.toString()),
annotation: jsonData['annotation']?.toString() ?? 'Нет описания',
coverUrl: null, // Will be set by the caller
pages: null,
language: 'Russian',
publishedYear: DateTime.now().year,
rating: 5.0,
status: 'want_to_read',
progress: null,
isFavorite: false,
);
return book;
} catch (e) {
print('Error analyzing book cover with OpenAI: $e');
return null;
}
}
String _normalizeGenre(String? genre) {
if (genre == null || genre.isEmpty) return 'other';
final normalized = genre.toLowerCase().trim();
// Map various genre names to our standard genres
final genreMap = {
'фантастика': 'fiction',
'fantasy': 'fantasy',
'фэнтези': 'fantasy',
'science': 'science',
'научпоп': 'science',
'научная': 'science',
'biography': 'biography',
'биография': 'biography',
'detective': 'detective',
'детектив': 'detective',
'роман': 'other',
'novel': 'other',
'poetry': 'other',
'поэзия': 'other',
};
return genreMap[normalized] ?? normalized;
}
}