PotatoHotDog는 무료 TTS (Text-to-Speech) API를 제공합니다. 이 API는 Speaker.bot이나 다른 제3자 미들웨어가 필요 없이 Streamer.bot에서 직접 호출할 수 있습니다. 이 가이드는 Streamer.bot의 Execute Code 하위 액션에 작은 사용자 정의 C# 스크립트를 추가하여 음성을 생성하고, 오디오를 다운로드한 뒤 자동으로 재생하는 방법을 보여줍니다.
복사해서 붙여넣을 수 있는 두 가지 즉시 사용 가능한 스크립트를 제공합니다. 하나는 고정된 Piper TTS 음성을 재생하고, 다른 하나는 당신의 복제 음성을 재생합니다. 두 스크립트 모두 당신의 PotatoHotDog API 키, 음성 ID, 그리고 말하고 싶은 텍스트만 필요합니다.
필수 매개변수
TTS를 생성하려면 Execute Code 하위 액션이 실행되기 전에 Streamer.bot에서 세 가지 인수를 지정해야 합니다. 다시 말해, 이 세 가지를 지정해야 합니다:
- API 키입니다.
- 생성하려는 음성의 ID입니다. 정적 음성 또는 클론 음성 중 하나일 수 있습니다.
- 텍스트.
다음과 같은 Streamer.bot 매개변수에 해당합니다:
- potatoKey
- potatoVoiceID
- potatoText
액션에서 인수를 지정하려면 Sub Actions에서 마우스 오른쪽 버튼을 클릭한 다음 Add, Core, Arguments를 차례로 선택하고 마지막으로 Set Argument를 선택합니다.
Streamer.bot에서 Execute Code 액션을 추가
Streamer.bot을 열고 Actions 탭으로 이동하세요. 새로운 액션을 만들거나 기존 액션을 열고, 나중에 알아볼 수 있는 이름으로 지정하세요. 예를 들어 "PotatoHotDog TTS"와 같은 이름으로 지으면 됩니다.
선택한 액션에서 Sub Actions 아래의 Add를 클릭한 다음 Core를 선택하고 이어서 Execute C# Code를 선택합니다. 이렇게 하면 Streamer.bot이 즉시 컴파일하고 실행하는 원시 C# 코드를 붙여넣을 수 있는 하위 동작이 추가됩니다.
다음에 있는 스크립트 중 하나를 코드 편집기에 붙여넣으세요. 정적 음성 재생을 원하시는지, 클론된 음성 재생을 원하시는지에 따라 달라집니다. 두 스크립트 모두 액션에 이미 존재하는 몇 가지 인자를 필요로 합니다. 인자 설정 방법은 아래의 필수 인자를 참고하세요.
스크립트를 붙인 뒤에는 Find Refs를 클릭하고 Compile을 클릭한 다음, 스크립트가 성공적으로 컴파일되었는지 확인해 주세요.
대시보드에서 PotatoHotDog API 키를 생성할 수 있습니다. 작업을 저장한 후 수동으로 트리거하거나 명령어나 채널 포인트 보상에 연결해 오디오가 올바르게 재생되는지 테스트하세요.
정적 음성을 재생하는 스크립트
이 스크립트는 정적 Piper TTS 보이스를 재생합니다. 음성을 생성하고, 처리가 끝날 때까지 기다린 뒤 생성된 오디오 파일을 다운로드하고, Streamer.bot에서 재생합니다. 사용 가능한 정적 보이스 ID 목록은 Static Voices 페이지에서 확인하실 수 있습니다.
정적 음성 둘러보기using System;
using System.IO;
using System.Net.Http;
using System.Threading;
using Newtonsoft.Json.Linq;
public class CPHInline
{
// Tracks the version of this script for troubleshooting and support.
private const string VERSION = "2 (static)";
// Base URL for the endpoint that creates a new TTS item.
private const string BASE_URL_GENERATE = "https://api-generate-static-voice-tts-item-v1.potatohotdog.com/";
// Base URL for the endpoint that polls a TTS item's status.
private const string BASE_URL_STATUS = "https://api-fetch-static-voice-tts-item-v1.potatohotdog.com/";
// Tracks the next numbered log argument.
private int logIndex = 0;
// Reuses one HTTP client for all API requests.
private static readonly HttpClient httpClient = new HttpClient
{
Timeout = TimeSpan.FromSeconds(30)
};
// Adds a log message as a numbered action argument.
private void Log(string message)
{
// Stores the message under a numbered key.
CPH.SetArgument($"potatoLog{logIndex}", message);
// Advances the log index for the next message.
logIndex++;
// Keeps the total log count in sync.
CPH.SetArgument("potatoLogCount", logIndex);
}
// Ensures a required action argument exists and is not empty.
private bool RequireArgument(string name, out string value)
{
// Fails when the argument is missing or blank.
if (!CPH.TryGetArg(name, out value) || string.IsNullOrWhiteSpace(value))
{
value = "";
return false;
}
return true;
}
// URL-encodes a value so it can safely be used in a query string.
private string EncodeUrlValue(string value)
{
// Escapes reserved characters for safe use in a URL.
return Uri.EscapeDataString(value);
}
// Performs an HTTP GET request and returns the response body.
private string HttpGet(string url)
{
try
{
Log($"GET {url}");
// Sends the HTTP request synchronously.
HttpResponseMessage response =
httpClient.GetAsync(url).GetAwaiter().GetResult();
// Reads the complete response body.
string responseBody =
response.Content.ReadAsStringAsync().GetAwaiter().GetResult();
Log($"HTTP status: {(int)response.StatusCode} {response.ReasonPhrase}");
// Stops when the server returns an unsuccessful status code.
if (!response.IsSuccessStatusCode)
{
Log($"Error: HTTP request failed with status {(int)response.StatusCode} {response.ReasonPhrase}.");
// Logs the server error response when available.
if (!string.IsNullOrWhiteSpace(responseBody))
{
Log($"Error response: {responseBody}");
}
return null;
}
Log($"Received {responseBody.Length} characters.");
return responseBody;
}
catch (Exception exception)
{
// Logs network, timeout, and other HTTP-related exceptions.
Log($"Error: HTTP request failed: {exception.Message}");
return null;
}
}
// Downloads a file from a URL and saves it to disk.
private bool DownloadFile(string url, string destinationPath)
{
try
{
Log($"Downloading audio from \"{url}\".");
Log($"Saving audio to \"{destinationPath}\".");
// Sends the audio download request.
HttpResponseMessage response =
httpClient.GetAsync(url).GetAwaiter().GetResult();
Log($"Audio download HTTP status: {(int)response.StatusCode} {response.ReasonPhrase}");
// Reads the complete audio response into memory.
byte[] fileData =
response.Content.ReadAsByteArrayAsync().GetAwaiter().GetResult();
// Stops when the audio server returns an unsuccessful status.
if (!response.IsSuccessStatusCode)
{
Log($"Error: Audio download failed with status {(int)response.StatusCode} {response.ReasonPhrase}.");
return false;
}
// Rejects an empty audio response.
if (fileData == null || fileData.Length == 0)
{
Log("Error: The downloaded audio file is empty.");
return false;
}
// Writes the downloaded audio bytes to the destination file.
File.WriteAllBytes(destinationPath, fileData);
Log($"Downloaded {fileData.Length} audio bytes.");
// Confirms that the audio file now exists on disk.
if (!File.Exists(destinationPath))
{
Log("Error: The audio download completed, but the file does not exist on disk.");
return false;
}
// Reports the final file size from disk.
FileInfo fileInfo = new FileInfo(destinationPath);
Log($"Audio file saved successfully. File size: {fileInfo.Length} bytes.");
return true;
}
catch (Exception exception)
{
// Logs download, filesystem, and permission errors.
Log($"Error: Could not download the audio file: {exception.Message}");
return false;
}
}
// Extracts a required string value from a JSON response.
private bool TryGetJsonString(string json, string propertyName, out string value)
{
value = "";
try
{
// Parses the response body as a JSON object.
JObject jsonObject = JObject.Parse(json);
// Retrieves the requested JSON property.
JToken property = jsonObject[propertyName];
if (property == null)
{
Log($"Error: JSON response does not contain \"{propertyName}\".");
return false;
}
value = property.ToString();
// Rejects empty JSON string values.
if (string.IsNullOrWhiteSpace(value))
{
Log($"Error: JSON property \"{propertyName}\" is empty.");
value = "";
return false;
}
return true;
}
catch (Exception exception)
{
// Logs malformed JSON and property conversion errors.
Log($"Error: Could not parse JSON property \"{propertyName}\": {exception.Message}");
return false;
}
}
// Extracts a required integer value from a JSON response.
private bool TryGetJsonInteger(string json, string propertyName, out int value)
{
value = 0;
try
{
// Parses the response body as a JSON object.
JObject jsonObject = JObject.Parse(json);
// Retrieves the requested JSON property.
JToken property = jsonObject[propertyName];
if (property == null)
{
Log($"Error: JSON response does not contain \"{propertyName}\".");
return false;
}
// Converts the JSON property to an integer.
if (!int.TryParse(property.ToString(), out value))
{
Log($"Error: JSON property \"{propertyName}\" is not a valid integer.");
value = 0;
return false;
}
return true;
}
catch (Exception exception)
{
// Logs malformed JSON and property conversion errors.
Log($"Error: Could not parse JSON property \"{propertyName}\": {exception.Message}");
return false;
}
}
// Creates a unique PotatoHotdog WAV filename.
private string CreateAudioFileName()
{
// Uses eight GUID characters to avoid overwriting previous files.
string uniqueID = Guid.NewGuid().ToString("N").Substring(0, 8).ToUpperInvariant();
return $"potatoAudio{uniqueID}.wav";
}
// Creates and returns the dedicated PotatoAudio folder.
private string GetAudioFolder()
{
// Uses Streamer.bot's application directory instead of the process working directory.
string streamerBotFolder = AppDomain.CurrentDomain.BaseDirectory;
// Places generated audio files inside a dedicated subfolder.
string audioFolder = Path.Combine(streamerBotFolder, "temp_potatohotdog_audio");
Log($"Streamer.bot folder: \"{streamerBotFolder}\".");
Log($"Audio folder: \"{audioFolder}\".");
// Creates the audio folder if it does not already exist.
if (!Directory.Exists(audioFolder))
{
Log("Audio folder does not exist. Creating it...");
Directory.CreateDirectory(audioFolder);
Log("Audio folder created.");
}
else
{
Log("Audio folder already exists.");
}
return audioFolder;
}
// Runs the main PotatoHotdog TTS action.
public bool Execute()
{
// Resets log numbering for this execution.
logIndex = 0;
CPH.SetArgument("potatoLogCount", 0);
Log("Starting script...");
// Logs the script version so runs can be tracked over time.
Log($"Version: {VERSION}");
// Retrieves the PotatoHotdog API key.
if (!RequireArgument("potatoKey", out string potatoKey))
{
Log("Error: potatoKey variable is missing. Please login to potatohotdog.com, generate an API key, create a set variable action and save the variable \"potatoKey\" with your key.");
return false;
}
// Retrieves the requested voice ID.
if (!RequireArgument("potatoVoiceID", out string potatoVoiceID))
{
Log("Error: potatoVoiceID variable is missing. Please login to potatohotdog.com, copy a voice ID, create a set variable action and save the variable \"potatoVoiceID\" with your voice ID.");
return false;
}
// Retrieves the text that should be converted to speech.
if (!RequireArgument("potatoText", out string potatoText))
{
Log("Error: potatoText variable is missing. Create a set variable action and save the variable \"potatoText\" with the text you want the TTS to say.");
return false;
}
// Logs the non-secret input values.
Log("potatoKey was provided.");
Log($"potatoVoiceID=\"{potatoVoiceID}\"");
Log($"potatoText=\"{potatoText}\"");
// Builds the API URL used to create the TTS item.
string createUrl = BASE_URL_GENERATE + "?key=" + EncodeUrlValue(potatoKey) + "&voice=" + EncodeUrlValue(potatoVoiceID) + "&text=" + EncodeUrlValue(potatoText);
// Requests creation of the TTS item.
string createJson = HttpGet(createUrl);
if (createJson == null)
{
Log("Error: Failed to create the TTS item.");
return false;
}
Log($"Create response: {createJson}");
// Extracts the created item's ID from the API response.
if (!TryGetJsonString(createJson, "itemID", out string itemID))
{
Log("Error: Could not retrieve itemID from the create response.");
return false;
}
Log($"itemID=\"{itemID}\"");
// Builds the API URL used to poll the TTS item status.
string fetchUrl = BASE_URL_STATUS + "?key=" + EncodeUrlValue(potatoKey) + "&item=" + EncodeUrlValue(itemID);
// Tracks whether the TTS item finishes processing.
bool completed = false;
// Stores the final fetch response containing the audio URL.
string completedFetchJson = null;
// Polls the item status up to 60 times.
for (int attempt = 1; attempt <= 60; attempt++)
{
Log($"Checking item status. Attempt {attempt} of 60.");
// Fetches the latest TTS item status.
string fetchJson = HttpGet(fetchUrl);
if (fetchJson == null)
{
Log($"Error: Failed to fetch the TTS item on attempt {attempt}.");
return false;
}
Log($"Fetch response: {fetchJson}");
// Extracts the current processing status from the response.
if (!TryGetJsonInteger(fetchJson, "statusID", out int statusID))
{
Log("Error: Could not retrieve statusID from the fetch response.");
return false;
}
Log($"statusID={statusID}");
// Stops polling when the API reports that processing is complete.
if (statusID == 0)
{
completed = true;
completedFetchJson = fetchJson;
Log("statusID is 0. TTS processing is complete.");
break;
}
// Waits five seconds before the next polling request.
if (attempt < 60)
{
Log("TTS processing is not complete. Waiting five seconds...");
Thread.Sleep(5000);
}
}
// Fails when the item has not completed after all polling attempts.
if (!completed)
{
Log("Error: TTS processing did not complete after 60 attempts.");
return false;
}
// Extracts the generated audio URL from the completed item response.
if (!TryGetJsonString(completedFetchJson, "audioURL", out string audioURL))
{
Log("Error: Could not retrieve audioURL from the completed fetch response.");
return false;
}
Log($"audioURL=\"{audioURL}\"");
string audioFolder;
try
{
// Resolves and creates the dedicated PotatoAudio directory.
audioFolder = GetAudioFolder();
}
catch (Exception exception)
{
Log($"Error: Could not create or access the audio folder: {exception.Message}");
return false;
}
// Creates a unique local filename for the generated WAV file.
string audioFileName = CreateAudioFileName();
// Combines the audio folder and unique filename.
string audioFilePath = Path.Combine(audioFolder, audioFileName);
Log($"Generated audio filename: \"{audioFileName}\".");
Log($"Local audio path: \"{audioFilePath}\".");
// Downloads the generated audio file to the PotatoAudio folder.
if (!DownloadFile(audioURL, audioFilePath))
{
Log("Error: Failed to download the generated TTS audio.");
return false;
}
try
{
Log($"Starting playback of \"{audioFilePath}\".");
// Plays the WAV file and waits until playback has finished.
CPH.PlaySound(audioFilePath, 1.0f, true, audioFileName, true);
Log("Audio playback finished.");
}
catch (Exception exception)
{
// Logs Streamer.bot audio playback errors.
Log($"Error: Could not play the audio file: {exception.Message}");
return false;
}
Log("Finished.");
return true;
}
}
클론된 음성을 재생하는 스크립트
이 스크립트는 위의 정적 음성 스크립트와 동일한 방식으로 작동하지만, 대신 복제 음성 중 하나를 사용해 음성을 생성합니다. 복제 음성 ID는 PotatoHotDog 대시보드의 Cloned Voices 페이지에서 확인할 수 있습니다.
나의 복제 음성 보기using System;
using System.IO;
using System.Net.Http;
using System.Threading;
using Newtonsoft.Json.Linq;
public class CPHInline
{
// Tracks the version of this script for troubleshooting and support.
private const string VERSION = "2 (cloned)";
// Base URL for the endpoint that creates a new TTS item.
private const string BASE_URL_GENERATE = "https://api-generate-cloned-voice-tts-item-v1.potatohotdog.com/";
// Base URL for the endpoint that polls a TTS item's status.
private const string BASE_URL_STATUS = "https://api-fetch-cloned-voice-tts-item-v1.potatohotdog.com/";
// Tracks the next numbered log argument.
private int logIndex = 0;
// Reuses one HTTP client for all API requests.
private static readonly HttpClient httpClient = new HttpClient
{
Timeout = TimeSpan.FromSeconds(30)
};
// Adds a log message as a numbered action argument.
private void Log(string message)
{
// Stores the message under a numbered key.
CPH.SetArgument($"potatoLog{logIndex}", message);
// Advances the log index for the next message.
logIndex++;
// Keeps the total log count in sync.
CPH.SetArgument("potatoLogCount", logIndex);
}
// Ensures a required action argument exists and is not empty.
private bool RequireArgument(string name, out string value)
{
// Fails when the argument is missing or blank.
if (!CPH.TryGetArg(name, out value) || string.IsNullOrWhiteSpace(value))
{
value = "";
return false;
}
return true;
}
// URL-encodes a value so it can safely be used in a query string.
private string EncodeUrlValue(string value)
{
// Escapes reserved characters for safe use in a URL.
return Uri.EscapeDataString(value);
}
// Performs an HTTP GET request and returns the response body.
private string HttpGet(string url)
{
try
{
Log($"GET {url}");
// Sends the HTTP request synchronously.
HttpResponseMessage response =
httpClient.GetAsync(url).GetAwaiter().GetResult();
// Reads the complete response body.
string responseBody =
response.Content.ReadAsStringAsync().GetAwaiter().GetResult();
Log($"HTTP status: {(int)response.StatusCode} {response.ReasonPhrase}");
// Stops when the server returns an unsuccessful status code.
if (!response.IsSuccessStatusCode)
{
Log($"Error: HTTP request failed with status {(int)response.StatusCode} {response.ReasonPhrase}.");
// Logs the server error response when available.
if (!string.IsNullOrWhiteSpace(responseBody))
{
Log($"Error response: {responseBody}");
}
return null;
}
Log($"Received {responseBody.Length} characters.");
return responseBody;
}
catch (Exception exception)
{
// Logs network, timeout, and other HTTP-related exceptions.
Log($"Error: HTTP request failed: {exception.Message}");
return null;
}
}
// Downloads a file from a URL and saves it to disk.
private bool DownloadFile(string url, string destinationPath)
{
try
{
Log($"Downloading audio from \"{url}\".");
Log($"Saving audio to \"{destinationPath}\".");
// Sends the audio download request.
HttpResponseMessage response =
httpClient.GetAsync(url).GetAwaiter().GetResult();
Log($"Audio download HTTP status: {(int)response.StatusCode} {response.ReasonPhrase}");
// Reads the complete audio response into memory.
byte[] fileData =
response.Content.ReadAsByteArrayAsync().GetAwaiter().GetResult();
// Stops when the audio server returns an unsuccessful status.
if (!response.IsSuccessStatusCode)
{
Log($"Error: Audio download failed with status {(int)response.StatusCode} {response.ReasonPhrase}.");
return false;
}
// Rejects an empty audio response.
if (fileData == null || fileData.Length == 0)
{
Log("Error: The downloaded audio file is empty.");
return false;
}
// Writes the downloaded audio bytes to the destination file.
File.WriteAllBytes(destinationPath, fileData);
Log($"Downloaded {fileData.Length} audio bytes.");
// Confirms that the audio file now exists on disk.
if (!File.Exists(destinationPath))
{
Log("Error: The audio download completed, but the file does not exist on disk.");
return false;
}
// Reports the final file size from disk.
FileInfo fileInfo = new FileInfo(destinationPath);
Log($"Audio file saved successfully. File size: {fileInfo.Length} bytes.");
return true;
}
catch (Exception exception)
{
// Logs download, filesystem, and permission errors.
Log($"Error: Could not download the audio file: {exception.Message}");
return false;
}
}
// Extracts a required string value from a JSON response.
private bool TryGetJsonString(string json, string propertyName, out string value)
{
value = "";
try
{
// Parses the response body as a JSON object.
JObject jsonObject = JObject.Parse(json);
// Retrieves the requested JSON property.
JToken property = jsonObject[propertyName];
if (property == null)
{
Log($"Error: JSON response does not contain \"{propertyName}\".");
return false;
}
value = property.ToString();
// Rejects empty JSON string values.
if (string.IsNullOrWhiteSpace(value))
{
Log($"Error: JSON property \"{propertyName}\" is empty.");
value = "";
return false;
}
return true;
}
catch (Exception exception)
{
// Logs malformed JSON and property conversion errors.
Log($"Error: Could not parse JSON property \"{propertyName}\": {exception.Message}");
return false;
}
}
// Extracts a required integer value from a JSON response.
private bool TryGetJsonInteger(string json, string propertyName, out int value)
{
value = 0;
try
{
// Parses the response body as a JSON object.
JObject jsonObject = JObject.Parse(json);
// Retrieves the requested JSON property.
JToken property = jsonObject[propertyName];
if (property == null)
{
Log($"Error: JSON response does not contain \"{propertyName}\".");
return false;
}
// Converts the JSON property to an integer.
if (!int.TryParse(property.ToString(), out value))
{
Log($"Error: JSON property \"{propertyName}\" is not a valid integer.");
value = 0;
return false;
}
return true;
}
catch (Exception exception)
{
// Logs malformed JSON and property conversion errors.
Log($"Error: Could not parse JSON property \"{propertyName}\": {exception.Message}");
return false;
}
}
// Creates a unique PotatoHotdog WAV filename.
private string CreateAudioFileName()
{
// Uses eight GUID characters to avoid overwriting previous files.
string uniqueID = Guid.NewGuid().ToString("N").Substring(0, 8).ToUpperInvariant();
return $"potatoAudio{uniqueID}.wav";
}
// Creates and returns the dedicated PotatoAudio folder.
private string GetAudioFolder()
{
// Uses Streamer.bot's application directory instead of the process working directory.
string streamerBotFolder = AppDomain.CurrentDomain.BaseDirectory;
// Places generated audio files inside a dedicated subfolder.
string audioFolder = Path.Combine(streamerBotFolder, "temp_potatohotdog_audio");
Log($"Streamer.bot folder: \"{streamerBotFolder}\".");
Log($"Audio folder: \"{audioFolder}\".");
// Creates the audio folder if it does not already exist.
if (!Directory.Exists(audioFolder))
{
Log("Audio folder does not exist. Creating it...");
Directory.CreateDirectory(audioFolder);
Log("Audio folder created.");
}
else
{
Log("Audio folder already exists.");
}
return audioFolder;
}
// Runs the main PotatoHotdog TTS action.
public bool Execute()
{
// Resets log numbering for this execution.
logIndex = 0;
CPH.SetArgument("potatoLogCount", 0);
Log("Starting script...");
// Logs the script version so runs can be tracked over time.
Log($"Version: {VERSION}");
// Retrieves the PotatoHotdog API key.
if (!RequireArgument("potatoKey", out string potatoKey))
{
Log("Error: potatoKey variable is missing. Please login to potatohotdog.com, generate an API key, create a set variable action and save the variable \"potatoKey\" with your key.");
return false;
}
// Retrieves the requested voice ID.
if (!RequireArgument("potatoVoiceID", out string potatoVoiceID))
{
Log("Error: potatoVoiceID variable is missing. Please login to potatohotdog.com, copy a voice ID, create a set variable action and save the variable \"potatoVoiceID\" with your voice ID.");
return false;
}
// Retrieves the text that should be converted to speech.
if (!RequireArgument("potatoText", out string potatoText))
{
Log("Error: potatoText variable is missing. Create a set variable action and save the variable \"potatoText\" with the text you want the TTS to say.");
return false;
}
// Logs the non-secret input values.
Log("potatoKey was provided.");
Log($"potatoVoiceID=\"{potatoVoiceID}\"");
Log($"potatoText=\"{potatoText}\"");
// Builds the API URL used to create the TTS item.
string createUrl = BASE_URL_GENERATE + "?key=" + EncodeUrlValue(potatoKey) + "&voice=" + EncodeUrlValue(potatoVoiceID) + "&text=" + EncodeUrlValue(potatoText);
// Requests creation of the TTS item.
string createJson = HttpGet(createUrl);
if (createJson == null)
{
Log("Error: Failed to create the TTS item.");
return false;
}
Log($"Create response: {createJson}");
// Extracts the created item's ID from the API response.
if (!TryGetJsonString(createJson, "itemID", out string itemID))
{
Log("Error: Could not retrieve itemID from the create response.");
return false;
}
Log($"itemID=\"{itemID}\"");
// Builds the API URL used to poll the TTS item status.
string fetchUrl = BASE_URL_STATUS + "?key=" + EncodeUrlValue(potatoKey) + "&item=" + EncodeUrlValue(itemID);
// Tracks whether the TTS item finishes processing.
bool completed = false;
// Stores the final fetch response containing the audio URL.
string completedFetchJson = null;
// Polls the item status up to 60 times.
for (int attempt = 1; attempt <= 60; attempt++)
{
Log($"Checking item status. Attempt {attempt} of 60.");
// Fetches the latest TTS item status.
string fetchJson = HttpGet(fetchUrl);
if (fetchJson == null)
{
Log($"Error: Failed to fetch the TTS item on attempt {attempt}.");
return false;
}
Log($"Fetch response: {fetchJson}");
// Extracts the current processing status from the response.
if (!TryGetJsonInteger(fetchJson, "statusID", out int statusID))
{
Log("Error: Could not retrieve statusID from the fetch response.");
return false;
}
Log($"statusID={statusID}");
// Stops polling when the API reports that processing is complete.
if (statusID == 0)
{
completed = true;
completedFetchJson = fetchJson;
Log("statusID is 0. TTS processing is complete.");
break;
}
// Waits five seconds before the next polling request.
if (attempt < 60)
{
Log("TTS processing is not complete. Waiting five seconds...");
Thread.Sleep(5000);
}
}
// Fails when the item has not completed after all polling attempts.
if (!completed)
{
Log("Error: TTS processing did not complete after 60 attempts.");
return false;
}
// Extracts the generated audio URL from the completed item response.
if (!TryGetJsonString(completedFetchJson, "audioURL", out string audioURL))
{
Log("Error: Could not retrieve audioURL from the completed fetch response.");
return false;
}
Log($"audioURL=\"{audioURL}\"");
string audioFolder;
try
{
// Resolves and creates the dedicated PotatoAudio directory.
audioFolder = GetAudioFolder();
}
catch (Exception exception)
{
Log($"Error: Could not create or access the audio folder: {exception.Message}");
return false;
}
// Creates a unique local filename for the generated WAV file.
string audioFileName = CreateAudioFileName();
// Combines the audio folder and unique filename.
string audioFilePath = Path.Combine(audioFolder, audioFileName);
Log($"Generated audio filename: \"{audioFileName}\".");
Log($"Local audio path: \"{audioFilePath}\".");
// Downloads the generated audio file to the PotatoAudio folder.
if (!DownloadFile(audioURL, audioFilePath))
{
Log("Error: Failed to download the generated TTS audio.");
return false;
}
try
{
Log($"Starting playback of \"{audioFilePath}\".");
// Plays the WAV file and waits until playback has finished.
CPH.PlaySound(audioFilePath, 1.0f, true, audioFileName, true);
Log("Audio playback finished.");
}
catch (Exception exception)
{
// Logs Streamer.bot audio playback errors.
Log($"Error: Could not play the audio file: {exception.Message}");
return false;
}
Log("Finished.");
return true;
}
}
문제 해결
스크립트가 기대대로 작동하지 않는 경우, 문제를 해결하는 가장 좋은 방법은 액션 로그를 확인하는 것입니다.
Action History에서 Action 변수들을 확인하면 이 작업을 수행할 수 있습니다.
이 가이드는 2026-07-11에 처음 게시되었고, 2026-07-11에 마지막으로 수정되었습니다.
다른 유사한 가이드들
다음은 여러분이 유용하게 보실 수 있는 다른 사용 방법 가이드들입니다.