OCR-to-TMDB Movie/Show Detection System

1 day ago 1
good, now write out all the logic for OCR parsing to get the right title, stars, year, and the right logic to query TMDB api and then further refine the results. Use psuedo code so I can give this document to another developer and they could implement it in another language. ⏺ OCR-to-TMDB Movie/Show Detection System Overview This system processes shared images containing movie/show information, extracts text via OCR, parses relevant metadata, queries TMDB API, and intelligently presents the best match to the user. Phase 1: Image Sharing & Storage FUNCTION handleSharedImage(imageData): // Store image in shared container for main app access sharedContainer = getSharedContainer("group.app.identifier") sharedImagePath = sharedContainer + "/shared_image.jpg" WRITE imageData TO sharedImagePath // Store path in shared UserDefaults sharedDefaults = getUserDefaults("group.app.identifier") sharedDefaults.set("sharedImagePath", sharedImagePath) LOG "Image saved to shared container: " + sharedImagePath END FUNCTION Phase 2: OCR Text Extraction FUNCTION performOCR(imagePath): image = loadImage(imagePath) IF image == NULL: LOG "Failed to load image" RETURN // Configure OCR for high accuracy ocrRequest = createTextRecognitionRequest() ocrRequest.recognitionLevel = ACCURATE ocrRequest.useLanguageCorrection = TRUE // Extract text with bounding box metadata results = performTextRecognition(image, ocrRequest) textWithMetadata = [] FOR EACH observation IN results: text = observation.topCandidate.string boundingBox = observation.boundingBox textWithMetadata.append({ text: text, boundingBox: boundingBox, fontSize: boundingBox.height // Relative font size }) LOG "OCR extracted " + textWithMetadata.length + " text elements" // Proceed to parsing parseMovieInfo(textWithMetadata) END FUNCTION Phase 3: Intelligent Text Parsing Font Size Analysis FUNCTION calculateFontMetrics(textElements): fontSizes = [] FOR EACH element IN textElements: fontSizes.append(element.fontSize) avgFontSize = average(fontSizes) maxFontSize = maximum(fontSizes) RETURN { avgFontSize, maxFontSize } END FUNCTION Title Detection FUNCTION parseMovieInfo(textWithMetadata): metrics = calculateFontMetrics(textWithMetadata) title = NULL year = NULL stars = [] titleCandidates = [] FOR EACH element IN textWithMetadata: text = element.text.trim() IF text.length < 2: CONTINUE // Extract year using regex yearMatch = REGEX_MATCH(text, "\b(19|20)\d{2}\b") IF yearMatch AND year == NULL: year = yearMatch LOG "Found year: " + year CONTINUE // Extract stars after "starring:" prefix IF text.toLowerCase().startsWith("starring:"): starText = text.substring(9).trim() starNames = starText.split(",") FOR EACH name IN starNames: cleanName = name.trim() IF cleanName.length > 2: stars.append(cleanName) LOG "Found stars: " + stars.join(", ") CONTINUE // Score potential titles titleScore = scoreTitleCandidate(text, element.index, element.fontSize, metrics) IF titleScore > 0: titleCandidates.append({ text: text, score: titleScore, index: element.index }) // Detect additional star names additionalStars = detectStarNames(textWithMetadata) stars.addAll(additionalStars) stars = removeDuplicates(stars) // Select best title IF titleCandidates.length > 0: // Sort by score (desc), then by index (asc) titleCandidates.sortBy(candidate => [-candidate.score, candidate.index]) validCandidates = titleCandidates.filter(c => !c.text.toLowerCase().contains("untitled") AND !c.text.contains("#") AND c.score > 30 ) IF validCandidates.length > 0: bestCandidate = validCandidates[0] title = buildCompleteTitle(textWithMetadata, bestCandidate, metrics.avgFontSize) // Proceed to TMDB query queryTMDBAPI(title, year, stars) END FUNCTION Title Scoring Algorithm FUNCTION scoreTitleCandidate(text, index, fontSize, metrics): score = 0 lowerText = text.toLowerCase() // EXCLUSIONS (return 0 if any match) excludeKeywords = ["watch", "play", "trailer", "director:", "starring:", "follows", "rating", "untitled", "mins", "hr"] FOR EACH keyword IN excludeKeywords: IF lowerText.contains(keyword): RETURN 0 // Exclude UI elements and codes IF text.contains("#") OR text.contains("=") OR text.contains("*"): RETURN 0 // Exclude purely numeric or short patterns IF text.length <= 2 OR REGEX_MATCH(text, "^[0-9]+$") OR REGEX_MATCH(text, "^[A-Z][0-9]+$"): RETURN 0 // POSITIVE SCORING // Font size bonus (most important for titles) fontRatio = fontSize / metrics.avgFontSize IF fontRatio >= 1.5: score += 30 // Very large font ELSE IF fontRatio >= 1.2: score += 20 // Larger than average ELSE IF fontRatio >= 1.0: score += 10 // Average or above // Maximum font size bonus IF fontSize == metrics.maxFontSize AND fontSize > metrics.avgFontSize: score += 25 // Position bonus (earlier text is more likely to be title) IF index <= 2: score += 20 ELSE IF index <= 5: score += 10 // Length scoring (sweet spot for titles) IF text.length >= 5 AND text.length <= 25: score += 15 ELSE IF text.length >= 3 AND text.length <= 40: score += 10 // All caps bonus (common for movie posters) IF text == text.toUpperCase() AND text.length >= 3: score += 10 // Title case bonus IF REGEX_MATCH(text, "^[A-Z][a-z]*(\s+[A-Z][a-z]*)*$"): score += 8 // Common title words titleWords = ["the", "a", "an", "of", "in", "on", "at", "to", "for", "with", "by"] IF titleWords.contains(lowerText): score += 15 RETURN score END FUNCTION Star Name Detection FUNCTION detectStarNames(textWithMetadata): detectedStars = [] processedIndices = SET() FOR index, element IN textWithMetadata: IF processedIndices.contains(index): CONTINUE text = element.text.trim() IF !isPotentialNameWord(text): CONTINUE // Look for first+last name combinations fullName = findFullName(index, textWithMetadata, processedIndices) IF fullName != NULL: detectedStars.append(fullName) RETURN detectedStars END FUNCTION FUNCTION isPotentialNameWord(text): // Must be reasonable length and all caps (typical for posters) IF text.length < 3 OR text.length > 12: RETURN FALSE IF text != text.toUpperCase(): RETURN FALSE // Exclude title words and UI elements excludeWords = ["THE", "TRUTH", "ABOUT", "AND", "UNTITLED"] IF excludeWords.contains(text): RETURN FALSE // Exclude numbers and symbols IF text.contains("#") OR text.contains("=") OR REGEX_MATCH(text, "^[0-9]+$"): RETURN FALSE RETURN TRUE END FUNCTION FUNCTION findFullName(startIndex, textElements, processedIndices): startText = textElements[startIndex].text.trim() // Look for companion name within next 2 positions FOR offset = 1 TO 2: searchIndex = startIndex + offset IF searchIndex >= textElements.length: BREAK IF processedIndices.contains(searchIndex): CONTINUE candidateText = textElements[searchIndex].text.trim() IF isPotentialNameWord(candidateText) AND isValidNameCombination(startText, candidateText): processedIndices.add(startIndex) processedIndices.add(searchIndex) RETURN startText + " " + candidateText RETURN NULL END FUNCTION Phase 4: TMDB API Query & Intelligent Filtering Initial Search FUNCTION queryTMDBAPI(title, year, stars): IF title == NULL OR title.isEmpty(): LOG "No title found, cannot query TMDB" RETURN LOG "Querying TMDB for: " + title + ", year: " + year + ", stars: " + stars allResults = [] // Search both movies and shows concurrently movieResults = searchMovies(title) showResults = searchShows(title) allResults.addAll(movieResults) allResults.addAll(showResults) // Apply intelligent filtering filterAndPresentResults(allResults, title, year, stars) END FUNCTION Multi-Stage Filtering FUNCTION filterAndPresentResults(allResults, searchTitle, searchYear, searchStars): LOG "Processing " + allResults.length + " total results" filteredResults = allResults // Stage 1: Filter by year if provided IF searchYear != NULL: yearInt = parseInt(searchYear) yearFilteredResults = filteredResults.filter(show => show.releaseYear == yearInt) LOG "After year filtering (" + searchYear + "): " + yearFilteredResults.length + " results" IF yearFilteredResults.length > 0: filteredResults = yearFilteredResults ELSE: LOG "No year matches, keeping all results" // Stage 2: Apply star filtering if multiple results and stars available IF searchStars.length > 0 AND filteredResults.length > 1: LOG "Multiple results found, checking cast info for star matches" checkCastAndPresentResults(filteredResults, searchTitle, searchStars) ELSE: presentFilteredResults(filteredResults, searchTitle) END FUNCTION Star-Based Cast Filtering FUNCTION checkCastAndPresentResults(results, searchTitle, searchStars): resultsWithStarMatches = [] // Fetch cast info for all results concurrently FOR EACH result IN results: IF result.mediaType == "movie": credits = fetchMovieCredits(result.id) ELSE: credits = fetchShowCredits(result.id) IF credits != NULL: matchCount = countStarMatches(credits.cast, searchStars) IF matchCount > 0: resultsWithStarMatches.append({ show: result, matchCount: matchCount }) LOG result.name + " (" + result.mediaType + "): " + matchCount + " star matches" // Analyze matches and decide presentation IF resultsWithStarMatches.length > 0: // Sort by match count (descending) resultsWithStarMatches.sortBy(item => -item.matchCount) bestMatch = resultsWithStarMatches[0] LOG "Best star match: " + bestMatch.show.name + " with " + bestMatch.matchCount + " matching stars" // Present directly if clear winner IF resultsWithStarMatches.length == 1 OR bestMatch.matchCount >= 2: LOG "Found clear winner with " + bestMatch.matchCount + " star matches" presentMovieDetail(bestMatch.show) ELSE: // Present filtered search results starFilteredResults = resultsWithStarMatches.map(item => item.show) LOG "Found " + starFilteredResults.length + " results with star matches" presentSearchResults(starFilteredResults, searchTitle) ELSE: // No star matches, present all original results LOG "No star matches found, presenting all results" presentFilteredResults(results, searchTitle) END FUNCTION Star Matching Algorithm FUNCTION countStarMatches(castMembers, searchStars): matchCount = 0 FOR EACH star IN searchStars: FOR EACH castMember IN castMembers: // Bidirectional fuzzy matching IF castMember.name.toLowerCase().contains(star.toLowerCase()) OR star.toLowerCase().contains(castMember.name.toLowerCase()): matchCount += 1 LOG "Star match: '" + star + "' matches '" + castMember.name + "'" BREAK // Don't count same star multiple times RETURN matchCount END FUNCTION Final Presentation Logic FUNCTION presentFilteredResults(results, searchTitle): IF results.length == 1: // Single result - present detail directly result = results[0] LOG "Found exactly 1 result: " + result.name presentMovieDetail(result) ELSE IF results.length > 1: // Multiple results - show search UI LOG "Found " + results.length + " results, presenting search UI" presentSearchResults(results, searchTitle) ELSE: LOG "No suitable results found for '" + searchTitle + "'" END FUNCTION FUNCTION presentMovieDetail(movie): // Present full detail view for the movie/show showDetailView(movie.id, movie.mediaType) END FUNCTION FUNCTION presentSearchResults(results, searchTitle): // Present list of results for user selection showSearchResultsList(results, searchTitle) END FUNCTION Key Algorithm Features 🎯 Intelligence Hierarchy 1. Font Size Analysis - Largest text likely to be title 2. Position Weighting - Earlier text prioritized 3. Pattern Recognition - Excludes UI elements, descriptions 4. Year Validation - Regex pattern matching 5. Star Name Detection - Combines individual names into full names 6. Multi-stage Filtering - Year → Stars → Presentation 🔍 Matching Strategies - Exact Year Matching - Filters by release year when available - Fuzzy Star Matching - Bidirectional substring matching for names - Score-based Title Selection - Weighted scoring system - Fallback Mechanisms - Graceful degradation when filtering fails 📊 Decision Matrix - Single Result → Present directly - Multiple with Clear Star Winner (≥2 matches) → Present directly - Multiple with Star Matches → Show filtered list - Multiple without Star Matches → Show all results - No Results → Log error This system provides robust, intelligent parsing with multiple validation layers and smart fallbacks for reliable movie/show identification from OCR text.
Read Entire Article