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.
- package/ios/ReactNativeKookitModule.swift +140 -73
- package/package.json +1 -1
|
@@ -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
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
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
|
-
|
|
1578
|
+
let synth = AVSpeechSynthesizer()
|
|
1579
|
+
synth.usesApplicationAudioSession = true
|
|
1580
|
+
self.speechSynthesizer = synth
|
|
1570
1581
|
}
|
|
1571
|
-
|
|
1572
|
-
|
|
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
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
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
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
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
|
-
//
|
|
1712
|
-
|
|
1713
|
-
|
|
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
|
-
//
|
|
1727
|
-
|
|
1728
|
-
let outputURL = ttsDir.appendingPathComponent(fileName)
|
|
1767
|
+
// Process non-empty buffer
|
|
1768
|
+
bufferCount += 1
|
|
1729
1769
|
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
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
|
-
|
|
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