Spaces:
Sleeping
Sleeping
| --[[ | |
| ReaSpeechAPI.lua - ReaSpeech API client | |
| ]]-- | |
| ReaSpeechAPI = { | |
| CURL_TIMEOUT_SECONDS = 5, | |
| base_url = nil, | |
| } | |
| function ReaSpeechAPI:init(host, protocol) | |
| protocol = protocol or 'http:' | |
| self.base_url = protocol .. '//' .. host | |
| end | |
| function ReaSpeechAPI:get_api_url(remote_path) | |
| local remote_path_no_leading_slash = remote_path:gsub("^/+", "") | |
| return ("%s/%s"):format(self.base_url, url.quote(remote_path_no_leading_slash)) | |
| end | |
| function ReaSpeechAPI:get_curl_cmd() | |
| local curl = "curl" | |
| if not reaper.GetOS():find("Win") then | |
| curl = "/usr/bin/curl" | |
| end | |
| return curl | |
| end | |
| function ReaSpeechAPI:fetch_json(url_path, http_method, error_handler, success_handler, timeout_handler, retry_count) | |
| http_method = http_method or 'GET' | |
| error_handler = error_handler or function(_msg) end | |
| success_handler = success_handler or function(_response) end | |
| timeout_handler = timeout_handler or function() end | |
| retry_count = retry_count or 0 | |
| local max_retries = 5 | |
| local retry_delay = 1 * (2 ^ retry_count) -- Exponential backoff | |
| local curl = self:get_curl_cmd() | |
| local api_url = self:get_api_url(url_path) | |
| local http_method_argument = "" | |
| if http_method ~= 'GET' then | |
| http_method_argument = " -X " .. http_method | |
| end | |
| local command = table.concat({ | |
| curl, | |
| ' "', api_url, '"', | |
| ' -H "accept: application/json"', | |
| http_method_argument, | |
| ' -m ', self.CURL_TIMEOUT_SECONDS, | |
| ' -s', | |
| ' -i', | |
| ' --http1.1', | |
| ' --retry 5', | |
| }) | |
| app:debug('Fetch JSON: ' .. command) | |
| local exec_result = (ExecProcess.new { command }):wait() | |
| if exec_result == nil then | |
| local msg = "Unable to run curl" | |
| app:log(msg) | |
| error_handler(msg) | |
| return | |
| end | |
| local status, output = exec_result:match("(%d+)\n(.*)") | |
| status = tonumber(status) | |
| if status == 28 then | |
| app:debug("Curl timeout reached") | |
| timeout_handler() | |
| return | |
| elseif status ~= 0 then | |
| local msg = "Curl failed with status " .. status | |
| app:debug(msg) | |
| error_handler(msg) | |
| return | |
| end | |
| local response_status, response_body = self.http_status_and_body(output) | |
| if response_status >= 500 and retry_count < max_retries then | |
| app:debug("Got 500 error, retrying in " .. retry_delay .. " seconds. Retry " .. (retry_count + 1) .. " of " .. max_retries) | |
| reaper.defer(function() | |
| self:fetch_json(url_path, http_method, error_handler, success_handler, timeout_handler, retry_count + 1) | |
| end) | |
| return | |
| elseif response_status >= 400 then | |
| local msg = "Request failed with status " .. response_status | |
| app:log(msg) | |
| error_handler(msg) | |
| return | |
| end | |
| local response_json = nil | |
| if app:trap(function() | |
| response_json = json.decode(response_body) | |
| end) then | |
| success_handler(response_json) | |
| else | |
| app:log("JSON parse error") | |
| app:log(output) | |
| error_handler("JSON parse error") | |
| end | |
| end | |
| -- Requests data that may be large or time-consuming. | |
| -- This method is non-blocking, and does not give any indication that it has | |
| -- completed. The path to the output file is returned. | |
| function ReaSpeechAPI:fetch_large(url_path, http_method) | |
| http_method = http_method or 'GET' | |
| local curl = self:get_curl_cmd() | |
| local api_url = self:get_api_url(url_path) | |
| local http_method_argument = "" | |
| if http_method ~= 'GET' then | |
| http_method_argument = " -X " .. http_method | |
| end | |
| local output_file = Tempfile:name() | |
| local sentinel_file = Tempfile:name() | |
| local command = table.concat({ | |
| curl, | |
| ' "', api_url, '"', | |
| ' -H "accept: application/json"', | |
| http_method_argument, | |
| ' -i ', | |
| ' -o "', output_file, '"', | |
| ' --http1.1', | |
| ' --retry 5', | |
| }) | |
| app:debug('Fetch large: ' .. command) | |
| local executor = ExecProcess.new { command, self.touch_cmd(sentinel_file) } | |
| if executor:background() then | |
| return output_file, sentinel_file | |
| else | |
| app:log("Unable to run curl") | |
| return nil | |
| end | |
| end | |
| ReaSpeechAPI.touch_cmd = function(filename) | |
| if reaper.GetOS():find("Win") then | |
| return 'echo. > "' .. filename .. '"' | |
| else | |
| return 'touch "' .. filename .. '"' | |
| end | |
| end | |
| -- Uploads a file to start a request for processing. | |
| -- This method is non-blocking, and does not give any indication that it has | |
| -- completed. The path to the output file is returned. | |
| function ReaSpeechAPI:post_request(url_path, data, file_path) | |
| local curl = self:get_curl_cmd() | |
| local api_url = self:get_api_url(url_path) | |
| local query = {} | |
| for k, v in pairs(data) do | |
| table.insert(query, k .. '=' .. url.quote(v)) | |
| end | |
| local output_file = Tempfile:name() | |
| local sentinel_file = Tempfile:name() | |
| local command = table.concat({ | |
| curl, | |
| ' "', api_url, '?', table.concat(query, '&'), '"', | |
| ' -H "accept: application/json"', | |
| ' -H "Content-Type: multipart/form-data"', | |
| ' -F ', self:_maybe_quote('audio_file=@"' .. file_path .. '"'), | |
| ' -i ', | |
| ' --http1.1 ', | |
| ' --retry 5', | |
| ' -o "', output_file, '"', | |
| }) | |
| app:log(file_path) | |
| app:debug('Post request: ' .. command) | |
| local executor = ExecProcess.new { command, self.touch_cmd(sentinel_file) } | |
| if executor:background() then | |
| return output_file, sentinel_file | |
| else | |
| app:log("Unable to run curl") | |
| return nil | |
| end | |
| end | |
| function ReaSpeechAPI:_maybe_quote(arg) | |
| if reaper.GetOS():find("Win") then | |
| return arg | |
| else | |
| return "'" .. arg .. "'" | |
| end | |
| end | |
| function ReaSpeechAPI.http_status_and_body(response) | |
| local headers, content = ReaSpeechAPI._split_curl_response(response) | |
| local last_status_line = headers[#headers] and headers[#headers][1] or '' | |
| local status = last_status_line:match("^HTTP/%d%.%d%s+(%d+)") | |
| if not status then | |
| local headers_log = "Headers array (status parsing failed):\n" | |
| for i, header_chunk in ipairs(headers) do | |
| headers_log = headers_log .. string.format("Chunk %d:\n", i) | |
| for j, header_line in ipairs(header_chunk) do | |
| headers_log = headers_log .. string.format(" %s\n", header_line) | |
| end | |
| end | |
| app:debug(headers_log) | |
| return -1, 'Status not found in headers' | |
| end | |
| local body = {} | |
| for _, chunk in pairs(content) do | |
| table.insert(body, table.concat(chunk, "\n")) | |
| end | |
| return tonumber(status), table.concat(body, "\n") | |
| end | |
| function ReaSpeechAPI._split_curl_response(input) | |
| local line_iterator = ReaSpeechAPI._line_iterator(input) | |
| local chunk_iterator = ReaSpeechAPI._chunk_iterator(line_iterator) | |
| local header_chunks = {} | |
| local content_chunks = {} | |
| local in_header = true | |
| for chunk in chunk_iterator do | |
| if in_header and chunk[1] and chunk[1]:match("^HTTP/%d%.%d") then | |
| table.insert(header_chunks, chunk) | |
| else | |
| in_header = false | |
| table.insert(content_chunks, chunk) | |
| end | |
| end | |
| return header_chunks, content_chunks | |
| end | |
| function ReaSpeechAPI._line_iterator(input) | |
| if type(input) == 'string' then | |
| local i = 1 | |
| local lines = {} | |
| for line in input:gmatch("([^\n]*)\n?") do | |
| table.insert(lines, line) | |
| end | |
| return function () | |
| local line = lines[i] | |
| i = i + 1 | |
| return line | |
| end | |
| else | |
| return input:lines() | |
| end | |
| end | |
| function ReaSpeechAPI._chunk_iterator(line_iterator) | |
| return function () | |
| local chunk = nil | |
| while true do | |
| local line = line_iterator() | |
| if line == nil or line:match("^%s*$") then break end | |
| chunk = chunk or {} | |
| table.insert(chunk, line) | |
| end | |
| return chunk | |
| end | |
| end | |