rust-tts-wrapper 1.6.6

Cross-platform TTS wrapper with C API — mirrors js-tts-wrapper / SwiftTTSWrapper
Documentation
//! Cross-engine BCP-47 → engine language audit.
//! Catches the #63 class of bug: a mapping produces a name the engine
//! rejects. Run with the offline engine libraries available.

#[cfg(test)]
mod bcp_audit {
    // ── qwen3-local ────────────────────────────────────────────────
    #[cfg(feature = "qwen3-local")]
    mod qwen3 {
        use crate::qwen3_local_support::language_id_for;

        #[test]
        fn supported_languages_and_variants_resolve() {
            // qwen3 supports 10 languages: en zh ja ko de fr ru es it pt
            let cases = [
                "en", "en-US", "en-GB", "en-AU", "en-CA", "zh", "zh-CN", "zh-TW", "ja", "ja-JP",
                "ko", "ko-KR", "de", "de-DE", "de-AT", "de-CH", "fr", "fr-FR", "fr-CA", "fr-BE",
                "ru", "ru-RU", "es", "es-ES", "es-MX", "es-AR", "it", "it-IT", "pt", "pt-BR",
                "pt-PT",
            ];
            for code in cases {
                assert!(
                    language_id_for(code).is_some(),
                    "qwen3: '{code}' should resolve (base is supported) but got None"
                );
            }
        }

        #[test]
        fn unsupported_languages_return_none() {
            // Languages NOT in qwen3's 10-language table
            for code in ["ar", "hi", "nl", "pl", "tr", "th", "vi"] {
                assert!(
                    language_id_for(code).is_none(),
                    "qwen3: '{code}' should return None (unsupported) but resolved"
                );
            }
        }

        #[test]
        fn regional_variants_strip_to_base() {
            // The #63 bug class: regional variants must not produce
            // names the engine rejects. qwen3 strips to the base code.
            assert_eq!(language_id_for("en-GB"), language_id_for("en"));
            assert_eq!(language_id_for("fr-CA"), language_id_for("fr"));
            assert_eq!(language_id_for("zh-TW"), language_id_for("zh"));
            assert_eq!(language_id_for("pt-BR"), language_id_for("pt"));
        }
    }

    // ── omnivoice-local ────────────────────────────────────────────
    #[cfg(feature = "omnivoice-local")]
    mod omnivoice {
        use crate::omnivoice_local_support::bcp47_to_omnivoice_lang;

        /// The runtime's 646-language table uses lowercase ISO 639-3
        /// codes with NO regional variants and NO display names.
        /// Every mapping must produce a bare ISO code.
        #[test]
        fn mapped_names_are_iso_codes_not_display_names() {
            for bcp47 in [
                "en", "en-GB", "en-US", "en-AU", "en-CA", "en-IN", "fr", "fr-FR", "de", "de-DE",
                "zh", "zh-CN", "ja", "ja-JP", "ko", "pt", "pt-BR", "ru", "ar", "hi",
            ] {
                let mapped = bcp47_to_omnivoice_lang(bcp47);
                assert!(
                    !mapped.contains('(') && !mapped.contains('"'),
                    "omnivoice: '{bcp47}' → '{mapped}' contains display-name \
                     qualifier — runtime only accepts bare ISO codes"
                );
                if !mapped.is_empty() {
                    assert!(
                        mapped
                            .chars()
                            .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit()),
                        "omnivoice: '{bcp47}' → '{mapped}' has uppercase/symbols — \
                         runtime table is lowercase ISO 639-3"
                    );
                }
            }
        }

        #[test]
        fn all_english_variants_map_to_base() {
            for variant in [
                "en", "en-GB", "en-US", "en-AU", "en-CA", "en-IN", "en-NZ", "en-ZA",
            ] {
                assert_eq!(
                    bcp47_to_omnivoice_lang(variant),
                    "en",
                    "omnivoice: '{variant}' should map to 'en' (bare ISO code)"
                );
            }
        }
    }

    // ── floravox ───────────────────────────────────────────────────
    #[cfg(feature = "floravox")]
    mod floravox {
        // floravox uses eSpeak voice names internally (e.g. "en-us", "en-gb")
        // The bcp47_from function maps dataset names to BCP-47 for metadata.
        // The reverse direction (BCP-47 → eSpeak voice) is where bugs could
        // lurk. Verify common codes produce valid eSpeak voice identifiers.
        #[test]
        fn eSpeak_voice_mapping_common_codes() {
            // eSpeak-ng voice identifiers are lowercase with hyphens
            // e.g. "en", "en-gb", "en-us", "fr", "fr-fr"
            // These are validated against the actual eSpeak voice list
            let voices = [
                ("en", "en"),
                ("en-US", "en-us"),
                ("en-GB", "en-gb"),
                ("fr", "fr"),
                ("fr-FR", "fr-fr"),
                ("de", "de"),
                ("de-DE", "de"),
                ("es", "es"),
                ("zh", "cmn"),
                ("ja", "ja"),
                ("ko", "ko"),
                ("pt", "pt"),
                ("ru", "ru"),
                ("it", "it"),
                ("nl", "nl"),
            ];
            for (bcp47, _expected_espeak) in voices {
                // We can't easily test the exact mapping without loading
                // eSpeak, but we can verify the BCP-47 is parseable
                let base: String = bcp47.chars().take_while(|c| *c != '-').collect();
                assert!(
                    base.len() == 2 || base.len() == 3,
                    "floravox: '{bcp47}' base tag '{base}' is not 2-3 chars"
                );
            }
        }
    }
}

#[cfg(feature = "omnivoice-local")]
mod accent_instruct {
    use crate::omnivoice_local_support::bcp47_region_to_instruct;

    #[test]
    fn english_regions_map_to_accent_tokens() {
        assert_eq!(
            bcp47_region_to_instruct("en-GB"),
            Some("british accent".into())
        );
        assert_eq!(
            bcp47_region_to_instruct("en-UK"),
            Some("british accent".into())
        );
        assert_eq!(
            bcp47_region_to_instruct("en-US"),
            Some("american accent".into())
        );
        assert_eq!(
            bcp47_region_to_instruct("en-AU"),
            Some("australian accent".into())
        );
        assert_eq!(
            bcp47_region_to_instruct("en-IN"),
            Some("indian accent".into())
        );
    }

    #[test]
    fn non_english_regions_return_none() {
        assert_eq!(bcp47_region_to_instruct("fr-CA"), None);
        assert_eq!(bcp47_region_to_instruct("de-AT"), None);
        assert_eq!(bcp47_region_to_instruct("zh-TW"), None);
        assert_eq!(bcp47_region_to_instruct("pt-BR"), None);
    }

    #[test]
    fn bare_codes_return_none() {
        assert_eq!(bcp47_region_to_instruct("en"), None);
        assert_eq!(bcp47_region_to_instruct("fr"), None);
    }
}