expo / expo

An open-source framework for making universal native apps with React. Expo runs on Android, iOS, and the web.
https://docs.expo.dev
MIT License
30.1k stars 4.72k forks source link

[SDK 40 & 41] getAvailableVoicesAsync() #11502

Closed Razorholt closed 2 years ago

Razorholt commented 3 years ago

Does getAvailableVoicesAsync() work on Android? I get nothing when querying the list of voices. Works fine on iOS.

const availableVoices = await Speech.getAvailableVoicesAsync();

AdamJNavarro commented 3 years ago

Hey @Razorholt, can you elaborate on what is being returned by the method? An empty array or nothing? Also, are you testing this on a physical device or an emulator?

I created a new project and I'm seeing the expected behavior and results.

Screen Shot 2021-01-03 at 11 15 18 AM
Razorholt commented 3 years ago

I tested it in several physical android devices from Samsung to Pixels. Same results:

const GetVoices = async()=>{
    let availableVoices = await Speech.getAvailableVoicesAsync();
    if (availableVoices){
        console.log('avail voices: ', availableVoices)
    }
}

returns:

Running application on LM-G710VM.
avail voices:  Array []
Razorholt commented 3 years ago

Is there a permission involved on android maybe?

Razorholt commented 3 years ago

https://snack.expo.io/@mobshed/e76230

Are you getting anything? I tried on several android devices and got nothing at all. Now, none of them were activated with any carrier, I'm using WIFI only. Would it make a difference?

xam7247 commented 3 years ago

For me, voices is populated only on the second call. It's always empty on the first call. And this is across all platforms.

rafaellupo commented 3 years ago

For me, voices is populated only on the second call. It's always empty on the first call. And this is across all platforms.

Exactly same here. First call returning an empty array. Next call returns all voices.

Razorholt commented 3 years ago

Any suggestions, code wise?

rafaellupo commented 3 years ago

Any suggestions, code wise?

Yes. Ugly but worked for me. I have created a 1000ms, 10x loop which tries to get available voices. It resolves at the first try 100% of the times I've tried. So you could just wait 1000ms, but for testing purposes I used the loop. Code following:

const availableVoices = await getAvailableVoicesAsync();
    if (availableVoices.length) {
      // Everything OK
    } else {
      // Try again
      console.log("No Voices Available. Trying again to get voices...");
      console.log("Waiting 1000ms");
      let tryAgainResult = [];
      for (let x = 0; x <= 10; x++) {
        await new Promise((resolve) => {
          setTimeout(() => {
            console.log("Trying Again. Retried ", x + 1, " times.");
            resolve(null);
          }, 1000);
        });
        const _availableVoices = await getAvailableVoicesAsync();
        if (_availableVoices.length) {
          tryAgainResult = _availableVoices;
          console.log("Apparently Had Success Trying. Voices: ", tryAgainResult);
          break;
        }
      }
   if (tryAgainResult.length) { 
      // Voices OK at tryAgainResult
   } else {
      throw new Error('Impossible to get Available Voices');
   }
Razorholt commented 3 years ago

Wow! Yeah, pretty ugly but whatever as long as it works. Thanks, man!

Razorholt commented 3 years ago

Ok, I think I got something:

let availableVoices = await Speech.getAvailableVoicesAsync();

availableVoices.length returns the proper number of characters availableVoices[0] returns json details of the first voice availableVoices returns nothing

Razorholt commented 3 years ago

And here is my solution, tested on iOS and Android (physical devices):

useEffect(()=>{ 
     const GetVoices = async()=>{
          let availableVoices;
          availableVoices = await Speech.getAvailableVoicesAsync();
          for (let x = 0; x <= availableVoices.length; x++) {
               if (availableVoices[x]) {
                    console.log(x, ' - ', availableVoices[x])
               } else {
                   break
               }
          }
     }
},[])

Snack here: https://snack.expo.io/@mobshed/expo-speech-getvoices

stale[bot] commented 2 years ago

It's been a while since we've had any activity on this issue, and seeing as it needs more info before we can properly address it, we will be closing it in one month. If you've found a fix, please share it! Otherwise, please provide the info we asked for, especially a reproducible example. Thanks!

AlenToma commented 2 years ago

Here is my solution that works

  const loadVoices = (counter?: number) => {
    setTimeout(async () => {
      var voices = await Speech.getAvailableVoicesAsync();

      if (voices.length > 0)
        setTextVoices(voices);
      else {
        console.log("voices not found")
        if (!counter || counter < 10)
          loadVoices((counter ?? 0) + 1);
      }
    }, (counter ?? 1) * 300);
  }
AlenToma commented 2 years ago

I am getting a new issue now. I implemented now a background service. When the app first start the voices get loaded. then I close the app but not the service and start the app again the voices do not load no matter how many time I try.

Dose anyone have the same problem ?

jamesbalcombe83 commented 2 years ago

We are having the same issue with our React-native application. Calling Speech.getAvailableVoicesAsync(); in a web build is fine and provides a list of voices. However, calling in an emulator or on a phone running the expo live bundle results in an empty array. I've tried all of the methods listed above to trick the call into populating the list, but none worked.

Razorholt commented 2 years ago

We are having the same issue with our React-native application. Calling Speech.getAvailableVoicesAsync(); in a web build is fine and provides a list of voices. However, calling in an emulator or on a phone running the expo live bundle results in an empty array. I've tried all of the methods listed above to trick the call into populating the list, but none worked.

You won't get any voices from an emulator. It has to be a real device

jamesbalcombe83 commented 2 years ago

We are having the same issue with our React-native application. Calling Speech.getAvailableVoicesAsync(); in a web build is fine and provides a list of voices. However, calling in an emulator or on a phone running the expo live bundle results in an empty array. I've tried all of the methods listed above to trick the call into populating the list, but none worked.

You won't get any voices from an emulator. It has to be a real device

I also ran this on a real phone from the expo QR code. Would this in effect be the same as an emulator?

Ashraf-Hamdoun commented 2 years ago

useEffect(() => { const GetVoices = async () => { let availableVoices; try { availableVoices = await Speech.getAvailableVoicesAsync().then((all) => { if (all.length === 0) { GetVoices(); } else { console.log(all); } }); } catch (error) { console.log("error is : ", error); } }; GetVoices(); }, []);

github-actions[bot] commented 2 years ago

This issue is stale because it has been open for 60 days with no activity. If there is no activity in the next 7 days, the issue will be closed.

AlenToma commented 2 years ago

The issue is still there friend, you cant close it.

msaqlain commented 2 years ago

Yes issue persists

github-actions[bot] commented 2 years ago

This issue is stale because it has been open for 60 days with no activity. If there is no activity in the next 7 days, the issue will be closed.

github-actions[bot] commented 2 years ago

This issue was closed because it has been inactive for 7 days since being marked as stale. Please open a new issue if you believe you are encountering a related problem.

vlazic commented 11 months ago

Here's my workaround:

If the first call to Speech.getAvailableVoicesAsync returns an empty array, the function will wait 1 second and try again, up to 10 times. This addresses the issue where the first call to getAvailableVoicesAsync often returns an empty array on Android.

// LanguageSelectComponent.tsx

import React, { useEffect } from "react";
import { Select, CheckIcon, HStack, Text } from "native-base";
import * as Speech from "expo-speech";
import { LanguageInfo, getLanguageNames, sortLanguages } from "../lib/language";

interface LanguageSelectComponentProps {
  initialLanguage: string;
  onLanguageChange: (language: string) => void;
}

const LanguageSelectComponent: React.FC<LanguageSelectComponentProps> = ({
  initialLanguage,
  onLanguageChange,
}) => {
  const [languages, setLanguages] = React.useState<LanguageInfo[]>([]);

  const getAvailableVoices = async () => {
    let voices = await Speech.getAvailableVoicesAsync();
    if (voices.length === 0) {
      for (let i = 0; i < 10; i++) {
        await new Promise(resolve => setTimeout(resolve, 1000));
        voices = await Speech.getAvailableVoicesAsync();
        if (voices.length > 0) break;
      }
    }

    const uniqueLanguages = Array.from(
      new Set(voices.map((voice) => voice.language)),
    );
    return uniqueLanguages;
  };

  useEffect(() => {
    getAvailableVoices()
      .then((langs) => {
        const languageNames = getLanguageNames(langs);
        const preferredLanguages = [
          "en-US",
          "en",
          "sr-RS",
          "sr",
          "hr-HR",
          "bs",
        ];
        const sortedLanguages = sortLanguages(
          languageNames,
          preferredLanguages,
        );
        setLanguages(sortedLanguages);
      })
      .catch((error) => {
        console.log(error);
      });
  }, []);

  return (
    <HStack space={2} alignItems="center">
      <Text w="30%" textAlign="center">
        Language:
      </Text>
      <Select
        selectedValue={initialLanguage}
        minWidth="200"
        accessibilityLabel="Choose Language"
        placeholder="Choose Language"
        _selectedItem={{
          bg: "teal.600",
          endIcon: <CheckIcon size="5" />,
        }}
        mt={1}
        onValueChange={onLanguageChange}
      >
        {languages.map((lang) => (
          <Select.Item label={lang.label} value={lang.code} key={lang.code} />
        ))}
      </Select>
    </HStack>
  );
};

export default LanguageSelectComponent;
// language.ts

import languageTags from 'language-tags';

interface LanguageInfo {
    code: string;
    label: string;
}

const getLanguageNames = (languages: string[]): LanguageInfo[] => {
    return languages
        .map((code) => ({
            code,
            language: languageTags(code).language(),
            region: languageTags(code).region(),
        }))
        .filter(({ language }) => language)
        .map(({ code, language, region }) => {
            const regionName = region?.descriptions()[0];

            return {
                code,
                label: region
                    ? `${language!.descriptions()[0]} (${regionName})`
                    : `${language!.descriptions()[0]}`,
            };
        });
};

function sortLanguages(languageNames: LanguageInfo[], prefferedLanguages: string[]) {
    return languageNames
        // make a copy of the array
        .concat()

        // first sort alphabetically by language name
        .sort((a, b) => {
            if (a.label < b.label) {
                return -1;
            } else if (a.label > b.label) {
                return 1;
            } else {
                return 0;
            }
        })

        // next sort by preffered languages
        .sort((a, b) => {
            const aIndex = prefferedLanguages.indexOf(a.code);
            const bIndex = prefferedLanguages.indexOf(b.code);
            if (aIndex === -1 && bIndex === -1) {
                return 0;
            } else if (aIndex === -1) {
                return 1;
            } else if (bIndex === -1) {
                return -1;
            } else {
                return aIndex - bIndex;
            }
        });
}

export { getLanguageNames, sortLanguages, LanguageInfo };
SerranoPablo commented 1 month ago

No solution yet? This loop to search voices 10 times sometimes does not work.

Somnus007 commented 2 weeks ago

I encountered a similar situation. I found that it was because I turned on the foreground service. That is to say, once I turned on a foreground service, I would no longer be able to get the available list after reopening the app. It was always an empty list. BTW, I start the foreground service via react-native-background-actions. Can anyone help?