@ztimson/utils 0.28.8 → 0.28.10

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/dist/index.d.ts CHANGED
@@ -21,5 +21,6 @@ export * from './search';
21
21
  export * from './string';
22
22
  export * from './template';
23
23
  export * from './time';
24
+ export * from './tts';
24
25
  export * from './types';
25
26
  export * from 'var-persist';
package/dist/index.mjs CHANGED
@@ -719,6 +719,10 @@ function pascalCase(str) {
719
719
  if (!str) return "";
720
720
  return wordSegments(str).map((w) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()).join("");
721
721
  }
722
+ function removeEmojis(str) {
723
+ const emojiRegex = /(?:[\u2700-\u27bf]|(?:\ud83c[\udde6-\uddff]){2}|[\ud83c[\udde6-\uddff]|[\ud83d[\ude00-\ude4f]|[\ud83d[\ude80-\udeff]|[\ud83c[\udd00-\uddff]|[\ud83d[\ude50-\ude7f]|[\u2600-\u26ff]|[\u2700-\u27bf]|[\ud83e[\udd00-\uddff]|[\ud83c[\udf00-\uffff]|[\ud83d[\ude00-\udeff]|[\ud83c[\udde6-\uddff])/g;
724
+ return str.replace(emojiRegex, "");
725
+ }
722
726
  function randomHex(length) {
723
727
  return Array(length).fill(null).map(() => Math.round(Math.random() * 15).toString(16)).join("");
724
728
  }
@@ -2527,6 +2531,160 @@ ${err.message || err.toString()}`);
2527
2531
  }
2528
2532
  return process(template);
2529
2533
  }
2534
+ class TTS {
2535
+ static QUALITY_PATTERNS = ["Google", "Microsoft", "Samantha", "Premium", "Natural", "Neural"];
2536
+ _currentUtterance = null;
2537
+ _voicesLoaded;
2538
+ _isStopping = false;
2539
+ _rate = 1;
2540
+ get rate() {
2541
+ return this._rate;
2542
+ }
2543
+ set rate(value) {
2544
+ this._rate = value;
2545
+ if (this._currentUtterance) this._currentUtterance.rate = value;
2546
+ }
2547
+ _pitch = 1;
2548
+ get pitch() {
2549
+ return this._pitch;
2550
+ }
2551
+ set pitch(value) {
2552
+ this._pitch = value;
2553
+ if (this._currentUtterance) this._currentUtterance.pitch = value;
2554
+ }
2555
+ _volume = 1;
2556
+ get volume() {
2557
+ return this._volume;
2558
+ }
2559
+ set volume(value) {
2560
+ this._volume = value;
2561
+ if (this._currentUtterance) this._currentUtterance.volume = value;
2562
+ }
2563
+ _voice;
2564
+ get voice() {
2565
+ return this._voice;
2566
+ }
2567
+ set voice(value) {
2568
+ this._voice = value;
2569
+ if (this._currentUtterance && value) this._currentUtterance.voice = value;
2570
+ }
2571
+ /** Create a TTS instance with optional configuration */
2572
+ constructor(config) {
2573
+ this._voicesLoaded = this.initializeVoices();
2574
+ if (config) {
2575
+ if (config.rate !== void 0) this._rate = config.rate;
2576
+ if (config.pitch !== void 0) this._pitch = config.pitch;
2577
+ if (config.volume !== void 0) this._volume = config.volume;
2578
+ this._voice = config.voice === null ? void 0 : config.voice || void 0;
2579
+ }
2580
+ }
2581
+ /** Initializes voice loading and sets default voice if needed */
2582
+ initializeVoices() {
2583
+ return new Promise((resolve) => {
2584
+ const voices = window.speechSynthesis.getVoices();
2585
+ if (voices.length > 0) {
2586
+ if (!this._voice) this._voice = TTS.bestVoice();
2587
+ resolve();
2588
+ } else {
2589
+ const handler = () => {
2590
+ window.speechSynthesis.removeEventListener("voiceschanged", handler);
2591
+ if (!this._voice) this._voice = TTS.bestVoice();
2592
+ resolve();
2593
+ };
2594
+ window.speechSynthesis.addEventListener("voiceschanged", handler);
2595
+ }
2596
+ });
2597
+ }
2598
+ /**
2599
+ * Selects the best available TTS voice, prioritizing high-quality options
2600
+ * @param lang Speaking language
2601
+ * @returns Highest quality voice
2602
+ */
2603
+ static bestVoice(lang = "en") {
2604
+ const voices = window.speechSynthesis.getVoices();
2605
+ for (const pattern of this.QUALITY_PATTERNS) {
2606
+ const voice = voices.find((v) => v.name.includes(pattern) && v.lang.startsWith(lang));
2607
+ if (voice) return voice;
2608
+ }
2609
+ return voices.find((v) => v.lang.startsWith(lang));
2610
+ }
2611
+ /** Cleans text for TTS by removing emojis, markdown and code block */
2612
+ static cleanText(text) {
2613
+ return removeEmojis(text).replace(/```[\s\S]*?```/g, " code block ").replace(/[#*_~`]/g, "");
2614
+ }
2615
+ /** Creates a speech utterance with current options */
2616
+ createUtterance(text) {
2617
+ const cleanedText = TTS.cleanText(text);
2618
+ const utterance = new SpeechSynthesisUtterance(cleanedText);
2619
+ const voice = this._voice || TTS.bestVoice();
2620
+ if (voice) utterance.voice = voice;
2621
+ utterance.rate = this._rate;
2622
+ utterance.pitch = this._pitch;
2623
+ utterance.volume = this._volume;
2624
+ return utterance;
2625
+ }
2626
+ /** Speaks text and returns a Promise which resolves once complete */
2627
+ async speak(text) {
2628
+ if (!text.trim()) return Promise.resolve();
2629
+ await this._voicesLoaded;
2630
+ return new Promise((resolve, reject) => {
2631
+ this._currentUtterance = this.createUtterance(text);
2632
+ this._currentUtterance.onend = () => {
2633
+ this._currentUtterance = null;
2634
+ resolve();
2635
+ };
2636
+ this._currentUtterance.onerror = (error) => {
2637
+ this._currentUtterance = null;
2638
+ if (this._isStopping && error.error === "interrupted") resolve();
2639
+ else reject(error);
2640
+ };
2641
+ window.speechSynthesis.speak(this._currentUtterance);
2642
+ });
2643
+ }
2644
+ /** Stops all TTS */
2645
+ stop() {
2646
+ this._isStopping = true;
2647
+ window.speechSynthesis.cancel();
2648
+ this._currentUtterance = null;
2649
+ setTimeout(() => {
2650
+ this._isStopping = false;
2651
+ }, 0);
2652
+ }
2653
+ /**
2654
+ * Initialize a stream that chunks text into sentences and speak them.
2655
+ *
2656
+ * @example
2657
+ * const stream = tts.speakStream();
2658
+ * stream.next("Hello ");
2659
+ * stream.next("World. How");
2660
+ * stream.next(" are you?");
2661
+ * await stream.done();
2662
+ *
2663
+ * @returns Object with next function for passing chunk of streamed text and done for completing the stream
2664
+ */
2665
+ speakStream() {
2666
+ let buffer = "";
2667
+ let streamPromise = Promise.resolve();
2668
+ const sentenceRegex = /[^.!?\n]+[.!?\n]+/g;
2669
+ return {
2670
+ next: (text) => {
2671
+ buffer += text;
2672
+ const sentences = buffer.match(sentenceRegex);
2673
+ if (sentences) {
2674
+ sentences.forEach((sentence) => streamPromise = this.speak(sentence.trim()));
2675
+ buffer = buffer.replace(sentenceRegex, "");
2676
+ }
2677
+ },
2678
+ done: async () => {
2679
+ if (buffer.trim()) {
2680
+ streamPromise = this.speak(buffer.trim());
2681
+ buffer = "";
2682
+ }
2683
+ await streamPromise;
2684
+ }
2685
+ };
2686
+ }
2687
+ }
2530
2688
  var dist = {};
2531
2689
  var persist = {};
2532
2690
  var hasRequiredPersist;
@@ -2750,6 +2908,7 @@ export {
2750
2908
  PromiseProgress,
2751
2909
  SYMBOL_LIST,
2752
2910
  ServiceUnavailableError,
2911
+ TTS,
2753
2912
  Table,
2754
2913
  TemplateError,
2755
2914
  TooManyRequestsError,
@@ -2816,6 +2975,7 @@ export {
2816
2975
  randomHex,
2817
2976
  randomString,
2818
2977
  randomStringBuilder,
2978
+ removeEmojis,
2819
2979
  renderTemplate,
2820
2980
  reservedIp,
2821
2981
  search,