react-native-kookit 0.3.7 → 0.3.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1551,25 +1551,41 @@ public class ReactNativeKookitModule: Module {
1551
1551
 
1552
1552
  // TTS (Text-to-Speech) Function
1553
1553
  AsyncFunction("synthesizeSpeech") { (options: [String: Any], promise: Promise) in
1554
- Task {
1555
- do {
1556
- let text = options["text"] as? String ?? ""
1557
- if text.isEmpty {
1558
- promise.reject("TTS_ERROR", "Text is required")
1559
- return
1560
- }
1561
-
1562
- let voiceId = options["voiceId"] as? String
1563
- let language = options["language"] as? String ?? "en-US"
1564
- let pitch = options["pitch"] as? Double ?? 1.0
1565
- let rate = options["rate"] as? Double ?? 1.0
1566
-
1567
- // Initialize synthesizer if needed
1554
+ // Extract parameters
1555
+ let text = options["text"] as? String ?? ""
1556
+ if text.isEmpty {
1557
+ promise.reject("TTS_ERROR", "Text is required")
1558
+ return
1559
+ }
1560
+
1561
+ let voiceId = options["voiceId"] as? String
1562
+ let language = options["language"] as? String ?? "en-US"
1563
+ let pitch = options["pitch"] as? Double ?? 1.0
1564
+ let rate = options["rate"] as? Double ?? 1.0
1565
+ //print the voiceId
1566
+ print("voiceId: \(voiceId)")
1567
+ //print the language
1568
+ print("language: \(language)")
1569
+ //print the pitch
1570
+ print("pitch: \(pitch)")
1571
+ //print the rate
1572
+ print("rate: \(rate)")
1573
+
1574
+ // Initialize synthesizer on main thread if needed
1575
+ if self.speechSynthesizer == nil {
1576
+ DispatchQueue.main.sync {
1568
1577
  if self.speechSynthesizer == nil {
1569
- self.speechSynthesizer = AVSpeechSynthesizer()
1578
+ let synth = AVSpeechSynthesizer()
1579
+ synth.usesApplicationAudioSession = true
1580
+ self.speechSynthesizer = synth
1570
1581
  }
1571
-
1572
- let filePath = try await self.synthesizeSpeechToFile(
1582
+ }
1583
+ }
1584
+
1585
+ // Perform synthesis on a background queue to avoid blocking
1586
+ DispatchQueue.global(qos: .userInitiated).async {
1587
+ do {
1588
+ let filePath = try self.synthesizeSpeechToFileSync(
1573
1589
  text: text,
1574
1590
  voiceId: voiceId,
1575
1591
  language: language,
@@ -1686,78 +1702,126 @@ public class ReactNativeKookitModule: Module {
1686
1702
 
1687
1703
  // MARK: - TTS Helper Methods
1688
1704
 
1689
- private func synthesizeSpeechToFile(text: String, voiceId: String?, language: String, pitch: Float, rate: Float) async throws -> String {
1690
- return try await withCheckedThrowingContinuation { continuation in
1691
- guard let synthesizer = self.speechSynthesizer else {
1692
- continuation.resume(throwing: NSError(domain: "TTS", code: -1, userInfo: [NSLocalizedDescriptionKey: "Speech synthesizer not initialized"]))
1693
- return
1694
- }
1695
-
1696
- let utterance = AVSpeechUtterance(string: text)
1697
-
1698
- // Set voice
1699
- if let voiceId = voiceId {
1700
- utterance.voice = AVSpeechSynthesisVoice(identifier: voiceId)
1705
+ // Synchronous version using write callback to generate audio file WITHOUT playing
1706
+ private func synthesizeSpeechToFileSync(text: String, voiceId: String?, language: String, pitch: Float, rate: Float) throws -> String {
1707
+ guard let synthesizer = self.speechSynthesizer else {
1708
+ throw NSError(domain: "TTS", code: -1, userInfo: [NSLocalizedDescriptionKey: "Speech synthesizer not initialized"])
1709
+ }
1710
+
1711
+ // Create utterance
1712
+ let utterance = AVSpeechUtterance(string: text)
1713
+
1714
+ // Set voice
1715
+ if let voiceId = voiceId {
1716
+ utterance.voice = AVSpeechSynthesisVoice(identifier: voiceId)
1717
+ } else {
1718
+ let voices = AVSpeechSynthesisVoice.speechVoices()
1719
+ if let voice = voices.first(where: { $0.language.hasPrefix(language) }) {
1720
+ utterance.voice = voice
1701
1721
  } else {
1702
- // Try to find voice by language
1703
- let voices = AVSpeechSynthesisVoice.speechVoices()
1704
- if let voice = voices.first(where: { $0.language.hasPrefix(language) }) {
1705
- utterance.voice = voice
1706
- } else {
1707
- utterance.voice = AVSpeechSynthesisVoice(language: language)
1708
- }
1722
+ utterance.voice = AVSpeechSynthesisVoice(language: language)
1723
+ }
1724
+ }
1725
+
1726
+ // Set pitch and rate
1727
+ utterance.pitchMultiplier = max(0.5, min(pitch, 2.0))
1728
+ let minRate = AVSpeechUtteranceMinimumSpeechRate
1729
+ let maxRate = AVSpeechUtteranceMaximumSpeechRate
1730
+ let targetRate = rate * AVSpeechUtteranceDefaultSpeechRate
1731
+ utterance.rate = min(max(targetRate, minRate), maxRate)
1732
+
1733
+ // Create TTS cache directory
1734
+ let cacheDir = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)[0]
1735
+ let ttsDir = cacheDir.appendingPathComponent("tts")
1736
+ try FileManager.default.createDirectory(at: ttsDir, withIntermediateDirectories: true, attributes: nil)
1737
+
1738
+ // Generate unique filename
1739
+ let fileName = "tts_\(UUID().uuidString).caf"
1740
+ let outputURL = ttsDir.appendingPathComponent(fileName)
1741
+
1742
+ // Synchronization
1743
+ let semaphore = DispatchSemaphore(value: 0)
1744
+ var audioFile: AVAudioFile?
1745
+ var capturedError: Error?
1746
+ var bufferCount = 0
1747
+
1748
+ print("🎤 Starting TTS audio generation (without playback)...")
1749
+
1750
+ // Use write callback to generate audio WITHOUT playing
1751
+ synthesizer.write(utterance) { buffer in
1752
+ // Check for completion: buffer with frameLength == 0 OR buffer is nil
1753
+ guard let pcmBuffer = buffer as? AVAudioPCMBuffer else {
1754
+ // buffer is nil or not PCM - this means completion
1755
+ print("✅ TTS generation completed, total buffers: \(bufferCount)")
1756
+ semaphore.signal()
1757
+ return
1709
1758
  }
1710
1759
 
1711
- // Set pitch and rate
1712
- utterance.pitchMultiplier = pitch
1713
- utterance.rate = rate * AVSpeechUtteranceDefaultSpeechRate
1714
-
1715
- // Create TTS cache directory
1716
- let cacheDir = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)[0]
1717
- let ttsDir = cacheDir.appendingPathComponent("tts")
1718
-
1719
- do {
1720
- try FileManager.default.createDirectory(at: ttsDir, withIntermediateDirectories: true, attributes: nil)
1721
- } catch {
1722
- continuation.resume(throwing: error)
1760
+ // Check if this is an empty buffer (completion signal)
1761
+ if pcmBuffer.frameLength == 0 {
1762
+ print("✅ TTS generation completed (empty buffer), total buffers: \(bufferCount)")
1763
+ semaphore.signal()
1723
1764
  return
1724
1765
  }
1725
1766
 
1726
- // Generate unique filename
1727
- let fileName = "tts_\(UUID().uuidString).caf"
1728
- let outputURL = ttsDir.appendingPathComponent(fileName)
1767
+ // Process non-empty buffer
1768
+ bufferCount += 1
1729
1769
 
1730
- // Write utterance to file using AVSpeechSynthesizer.write
1731
- Task {
1732
- do {
1733
- var audioBuffers: [AVAudioPCMBuffer] = []
1734
-
1735
- synthesizer.write(utterance) { buffer in
1736
- if let buffer = buffer as? AVAudioPCMBuffer {
1737
- audioBuffers.append(buffer)
1738
- } else if buffer == nil {
1739
- // Writing is complete when buffer is nil
1740
- Task {
1741
- do {
1742
- // Write all buffers to file
1743
- try self.writeBuffersToFile(buffers: audioBuffers, url: outputURL)
1744
- continuation.resume(returning: outputURL.path)
1745
- } catch {
1746
- continuation.resume(throwing: error)
1747
- }
1748
- }
1749
- }
1750
- }
1770
+ do {
1771
+ // Create audio file on first buffer
1772
+ if audioFile == nil {
1773
+ print("📝 Creating audio file with format: \(pcmBuffer.format)")
1774
+ audioFile = try AVAudioFile(
1775
+ forWriting: outputURL,
1776
+ settings: pcmBuffer.format.settings
1777
+ )
1778
+ }
1779
+
1780
+ // Write buffer to file
1781
+ try audioFile?.write(from: pcmBuffer)
1782
+
1783
+ if bufferCount % 10 == 0 {
1784
+ print("📦 Written \(bufferCount) buffers...")
1751
1785
  }
1786
+ } catch {
1787
+ print("❌ Error writing buffer: \(error)")
1788
+ capturedError = error
1789
+ semaphore.signal()
1752
1790
  }
1753
1791
  }
1792
+
1793
+ // Wait for completion
1794
+ print("⏳ Waiting for audio generation to complete...")
1795
+ semaphore.wait()
1796
+
1797
+ // Check for errors
1798
+ if let error = capturedError {
1799
+ throw error
1800
+ }
1801
+
1802
+ // Verify audio file was created
1803
+ guard audioFile != nil else {
1804
+ throw NSError(domain: "TTS", code: -1, userInfo: [NSLocalizedDescriptionKey: "No audio data was generated"])
1805
+ }
1806
+
1807
+ print("✅ Audio file created: \(outputURL.path)")
1808
+ return outputURL.path
1754
1809
  }
1755
1810
 
1756
1811
  private func writeBuffersToFile(buffers: [AVAudioPCMBuffer], url: URL) throws {
1812
+ guard !buffers.isEmpty else {
1813
+ throw NSError(domain: "TTS", code: -1, userInfo: [NSLocalizedDescriptionKey: "No audio data generated"])
1814
+ }
1815
+
1757
1816
  guard let firstBuffer = buffers.first else {
1758
1817
  throw NSError(domain: "TTS", code: -1, userInfo: [NSLocalizedDescriptionKey: "No audio data generated"])
1759
1818
  }
1760
1819
 
1820
+ // Verify that the buffer has valid data
1821
+ guard firstBuffer.frameLength > 0 else {
1822
+ throw NSError(domain: "TTS", code: -1, userInfo: [NSLocalizedDescriptionKey: "Audio buffer is empty (frameLength is 0)"])
1823
+ }
1824
+
1761
1825
  // Create audio file with settings from the buffer
1762
1826
  let settings: [String: Any] = [
1763
1827
  AVFormatIDKey: kAudioFormatLinearPCM,
@@ -1772,7 +1836,10 @@ public class ReactNativeKookitModule: Module {
1772
1836
 
1773
1837
  // Write all buffers to file
1774
1838
  for buffer in buffers {
1775
- try audioFile.write(from: buffer)
1839
+ // Skip empty buffers
1840
+ if buffer.frameLength > 0 {
1841
+ try audioFile.write(from: buffer)
1842
+ }
1776
1843
  }
1777
1844
  }
1778
1845
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-kookit",
3
- "version": "0.3.7",
3
+ "version": "0.3.8",
4
4
  "description": "React Native module for intercepting volume button presses on iOS and Android, with FTP client functionality",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",