Explorar o código

Speed up long-text processing with chunked parallel API requests.

Process paraphrasing, summarizer, and punctuation checks in parallel chunks and trim journal finder prompts so 1,000-word inputs return faster.

Co-authored-by: Cursor <cursoragent@cursor.com>
AhtashamShahzad1 hai 1 mes
pai
achega
4486fa9dfb

+ 10 - 1
gramora/Services/JournalFinderService.swift

@@ -5,6 +5,8 @@ protocol JournalFinderServicing {
 }
 
 struct JournalFinderService: JournalFinderServicing {
+    private static let abstractPromptWordLimit = 320
+
     private let openAI: OpenAIChatServicing
 
     init(openAI: OpenAIChatServicing = OpenAIChatService.shared) {
@@ -86,7 +88,14 @@ struct JournalFinderService: JournalFinderServicing {
             lines.append("Title: \(title)")
         }
         if !abstract.isEmpty {
-            lines.append("Abstract / Summary: \(abstract)")
+            let condensedAbstract = abstract.truncated(toMaxWords: Self.abstractPromptWordLimit)
+            let isTruncated = condensedAbstract.wordCount < abstract.wordCount
+            if isTruncated {
+                lines.append("Abstract / Summary (first \(Self.abstractPromptWordLimit) words): \(condensedAbstract)")
+                lines.append("Note: Use this abstract excerpt plus title/keywords to infer journal fit.")
+            } else {
+                lines.append("Abstract / Summary: \(condensedAbstract)")
+            }
         }
         if !keywords.isEmpty {
             lines.append("Keywords: \(keywords)")

+ 46 - 7
gramora/Services/ParaphrasingService.swift

@@ -5,6 +5,13 @@ protocol ParaphrasingServicing {
 }
 
 struct ParaphrasingService: ParaphrasingServicing {
+    private static let chunkMaxWords = 280
+    private static let maxConcurrentChunks = 4
+    private static let systemPrompt = """
+    You are a professional writing assistant. Paraphrase the user's text while preserving \
+    the original meaning and tone. Return only the paraphrased text with no preamble or explanation.
+    """
+
     private let openAI: OpenAIChatServicing
 
     init(openAI: OpenAIChatServicing = OpenAIChatService.shared) {
@@ -17,12 +24,44 @@ struct ParaphrasingService: ParaphrasingServicing {
             throw OpenAIServiceError.apiError("Enter some text to paraphrase.")
         }
 
-        return try await openAI.completeText(
-            systemPrompt: """
-            You are a professional writing assistant. Paraphrase the user's text while preserving \
-            the original meaning and tone. Return only the paraphrased text with no preamble or explanation.
-            """,
-            userPrompt: trimmed
-        )
+        let chunks = trimmed.chunked(maxWords: Self.chunkMaxWords)
+        if chunks.count == 1 {
+            return try await openAI.completeText(
+                systemPrompt: Self.systemPrompt,
+                userPrompt: trimmed,
+                maxTokens: 1_000
+            )
+        }
+
+        var paraphrasedChunks: [String] = []
+
+        for batchStart in stride(from: 0, to: chunks.count, by: Self.maxConcurrentChunks) {
+            let batchEnd = min(batchStart + Self.maxConcurrentChunks, chunks.count)
+            let batch = Array(chunks[batchStart..<batchEnd])
+
+            let batchResults = try await withThrowingTaskGroup(of: (Int, String).self) { group in
+                for (offset, chunk) in batch.enumerated() {
+                    let originalIndex = batchStart + offset
+                    group.addTask {
+                        let paraphrased = try await self.openAI.completeText(
+                            systemPrompt: Self.systemPrompt,
+                            userPrompt: chunk,
+                            maxTokens: 500
+                        )
+                        return (originalIndex, paraphrased)
+                    }
+                }
+
+                var indexedResults: [(Int, String)] = []
+                for try await item in group {
+                    indexedResults.append(item)
+                }
+                return indexedResults.sorted { $0.0 < $1.0 }
+            }
+
+            paraphrasedChunks.append(contentsOf: batchResults.map(\.1))
+        }
+
+        return paraphrasedChunks.joined(separator: "\n\n")
     }
 }

+ 70 - 21
gramora/Services/PunctuationCheckService.swift

@@ -5,6 +5,24 @@ protocol PunctuationCheckServicing {
 }
 
 struct PunctuationCheckService: PunctuationCheckServicing {
+    private static let chunkMaxWords = 280
+    private static let maxConcurrentChunks = 4
+    private static let systemPrompt = """
+    You are a professional punctuation checker. Analyze the user's text for punctuation \
+    issues only. Respond with JSON only using this schema:
+    {
+      "correctedText": "corrected version of this chunk only",
+      "issues": [
+        {
+          "original": "problematic phrase",
+          "suggestion": "corrected phrase",
+          "explanation": "brief reason for the change"
+        }
+      ]
+    }
+    If there are no issues, return the original chunk in correctedText and an empty issues array.
+    """
+
     private let openAI: OpenAIChatServicing
 
     init(openAI: OpenAIChatServicing = OpenAIChatService.shared) {
@@ -17,31 +35,62 @@ struct PunctuationCheckService: PunctuationCheckServicing {
             throw GrammarCheckServiceError.emptyInput
         }
 
-        let response: TextIssuesResponse = try await openAI.completeJSON(
-            systemPrompt: """
-            You are a professional punctuation checker. Analyze the user's text for punctuation \
-            issues only. Respond with JSON only using this schema:
-            {
-              "correctedText": "full corrected version of the input",
-              "issues": [
-                {
-                  "original": "problematic phrase",
-                  "suggestion": "corrected phrase",
-                  "explanation": "brief reason for the change"
+        let chunks = trimmed.chunked(maxWords: Self.chunkMaxWords)
+        if chunks.count == 1 {
+            let response: TextIssuesResponse = try await openAI.completeJSON(
+                systemPrompt: Self.systemPrompt,
+                userPrompt: trimmed,
+                maxTokens: 1_000,
+                as: TextIssuesResponse.self
+            )
+
+            return GrammarCheckResult(
+                correctedText: response.correctedText,
+                issues: response.issues.map {
+                    GrammarIssue(original: $0.original, suggestion: $0.suggestion, explanation: $0.explanation)
+                }
+            )
+        }
+
+        var correctedChunks: [String] = []
+        var allIssues: [GrammarIssue] = []
+
+        for batchStart in stride(from: 0, to: chunks.count, by: Self.maxConcurrentChunks) {
+            let batchEnd = min(batchStart + Self.maxConcurrentChunks, chunks.count)
+            let batch = Array(chunks[batchStart..<batchEnd])
+
+            let batchResults = try await withThrowingTaskGroup(of: (Int, TextIssuesResponse).self) { group in
+                for (offset, chunk) in batch.enumerated() {
+                    let originalIndex = batchStart + offset
+                    group.addTask {
+                        let response: TextIssuesResponse = try await self.openAI.completeJSON(
+                            systemPrompt: Self.systemPrompt,
+                            userPrompt: chunk,
+                            maxTokens: 550,
+                            as: TextIssuesResponse.self
+                        )
+                        return (originalIndex, response)
+                    }
+                }
+
+                var indexedResults: [(Int, TextIssuesResponse)] = []
+                for try await item in group {
+                    indexedResults.append(item)
                 }
-              ]
+                return indexedResults.sorted { $0.0 < $1.0 }
             }
-            If there are no issues, return the original text in correctedText and an empty issues array.
-            """,
-            userPrompt: trimmed,
-            as: TextIssuesResponse.self
-        )
+
+            correctedChunks.append(contentsOf: batchResults.map(\.1.correctedText))
+            allIssues.append(contentsOf: batchResults.flatMap { result in
+                result.1.issues.map {
+                    GrammarIssue(original: $0.original, suggestion: $0.suggestion, explanation: $0.explanation)
+                }
+            })
+        }
 
         return GrammarCheckResult(
-            correctedText: response.correctedText,
-            issues: response.issues.map {
-                GrammarIssue(original: $0.original, suggestion: $0.suggestion, explanation: $0.explanation)
-            }
+            correctedText: correctedChunks.joined(separator: "\n\n"),
+            issues: allIssues
         )
     }
 }

+ 52 - 5
gramora/Services/SummarizerService.swift

@@ -5,6 +5,17 @@ protocol SummarizerServicing {
 }
 
 struct SummarizerService: SummarizerServicing {
+    private static let chunkMaxWords = 350
+    private static let maxConcurrentChunks = 4
+    private static let chunkSummaryPrompt = """
+    You are a professional summarizer. Produce a concise summary of this text fragment \
+    in 2 to 4 short bullet points. Return only the bullet points.
+    """
+    private static let finalSummaryPrompt = """
+    You are a professional summarizer. Combine the fragment summaries into one clear, concise \
+    final summary for the user. Return only the summary with no preamble or explanation.
+    """
+
     private let openAI: OpenAIChatServicing
 
     init(openAI: OpenAIChatServicing = OpenAIChatService.shared) {
@@ -17,12 +28,48 @@ struct SummarizerService: SummarizerServicing {
             throw OpenAIServiceError.apiError("Enter some text to summarize.")
         }
 
+        let chunks = trimmed.chunked(maxWords: Self.chunkMaxWords)
+        if chunks.count == 1 {
+            return try await openAI.completeText(
+                systemPrompt: Self.finalSummaryPrompt,
+                userPrompt: trimmed,
+                maxTokens: 280
+            )
+        }
+
+        var partialSummaries: [String] = []
+
+        for batchStart in stride(from: 0, to: chunks.count, by: Self.maxConcurrentChunks) {
+            let batchEnd = min(batchStart + Self.maxConcurrentChunks, chunks.count)
+            let batch = Array(chunks[batchStart..<batchEnd])
+
+            let batchResults = try await withThrowingTaskGroup(of: (Int, String).self) { group in
+                for (offset, chunk) in batch.enumerated() {
+                    let originalIndex = batchStart + offset
+                    group.addTask {
+                        let chunkSummary = try await self.openAI.completeText(
+                            systemPrompt: Self.chunkSummaryPrompt,
+                            userPrompt: chunk,
+                            maxTokens: 180
+                        )
+                        return (originalIndex, chunkSummary)
+                    }
+                }
+
+                var indexedResults: [(Int, String)] = []
+                for try await item in group {
+                    indexedResults.append(item)
+                }
+                return indexedResults.sorted { $0.0 < $1.0 }
+            }
+
+            partialSummaries.append(contentsOf: batchResults.map(\.1))
+        }
+
         return try await openAI.completeText(
-            systemPrompt: """
-            You are a professional summarizer. Produce a clear, concise summary of the user's text. \
-            Return only the summary with no preamble or explanation.
-            """,
-            userPrompt: trimmed
+            systemPrompt: Self.finalSummaryPrompt,
+            userPrompt: partialSummaries.joined(separator: "\n\n"),
+            maxTokens: 260
         )
     }
 }