--[[ Workspace > Scripts > Qawlak Round-trip -- DaVinci Resolve menu script in Lua (free edition 21.x). Captions the current timeline and puts the SRT on subtitle track 1 at the timeline start. Install next to "Qawlak Import.lua" (this script uses its helpers): %APPDATA%\Blackmagic Design\DaVinci Resolve\Support\Fusion\Scripts\Utility\ From the menu (P15-46): free Resolve's menu scripts cannot read files or reach the network (no ffi, io, os.execute or require; measured live, 15 Sep 2026), so this renders the timeline audio into %APPDATA%\Qawlak\resolve-queue and waits for the Qawlak helper (docs/plugins/resolve-helper, running outside Resolve) to answer with the captions. The helper holds the key file, uploads, transcribes, and shows low-balance, credit-wall and refusal messages as notifications. Progress shows as a marker at the timeline start (free Resolve answers a Fusion window with the Studio upsell). Free Resolve appends subtitle items after the timeline's end whatever recordFrame says; when that happens the cues are taken back off, the SRT stays in the Media Pool, and the marker says so. Status language: M.LANGUAGE in "Qawlak Import.lua". From the Console (F6), which has ffi: the direct round trip below still runs without the helper, reading %APPDATA%\Qawlak\resolve.json ({"key": "qwlk_...", "url": "http://127.0.0.1:8765", "language": "ar", "alpha": false}). HTTP goes through Windows' own curl.exe; the key travels in a header file in %TEMP% deleted after each call. --]] -- K31: install.ps1 writes one copy of this file per extra menu entry with CHOICE set: "Qawlak Round-trip (Gulf).lua" = 'gulf' -- (also arabic, egyptian, levantine, maghrebi, msa, english, hindi, urdu), and "Qawlak Cancel.lua" = 'cancel'. nil: the -- language and dialect in resolve.json. local CHOICE = nil local function load_helpers() local candidates = {} local dbg = rawget(_G, 'debug') -- may be absent in Resolve's Lua if dbg and dbg.getinfo then local source = (dbg.getinfo(1, 'S').source or ''):gsub('^@', '') candidates[#candidates + 1] = (source:match('^(.*[\\/])') or '') .. 'Qawlak Import.lua' end local ok, appdata = pcall(os.getenv, 'APPDATA') if ok and appdata then candidates[#candidates + 1] = appdata .. '\\Blackmagic Design\\DaVinci Resolve\\Support\\Fusion\\Scripts\\Utility\\Qawlak Import.lua' end for _, path in ipairs(candidates) do local chunk = loadfile(path) if chunk then local loaded, helpers = pcall(chunk, 'qawlak-lib') if loaded and type(helpers) == 'table' then return helpers end end end end local M = load_helpers() local ffi = rawget(_G, 'ffi') if not M then local message = 'Qawlak: put "Qawlak Import.lua" in the same Scripts folder as "Qawlak Round-trip.lua".' pcall(function() -- print shows nowhere from the menu, so say it on the timeline too local timeline = resolve:GetProjectManager():GetCurrentProject():GetCurrentTimeline() timeline:AddMarker(0, 'Red', 'Qawlak', message, 1, 'qawlak') end) return print(message) end local SEP, TEMP = M.SEP, os.getenv('TEMP') or '/tmp' -- ------------------------------------------------------------------ Console path (ffi) local function wait(seconds) if bmd and bmd.wait then bmd.wait(seconds) else M.run(M.WINDOWS and ('ping -n ' .. (seconds + 1) .. ' 127.0.0.1') or ('sleep ' .. seconds)) end end local function config() local base = os.getenv('APPDATA') and (os.getenv('APPDATA') .. '\\Qawlak') or ((os.getenv('HOME') or '') .. '/.config/qawlak') local path = base .. SEP .. 'resolve.json' local cfg = M.parse_config(M.read(path) or '') cfg.key = os.getenv('QAWLAK_KEY') or cfg.key cfg.url = (os.getenv('QAWLAK_URL') or cfg.url or 'http://127.0.0.1:8765'):gsub('/+$', '') cfg.path = path return cfg end -- One call to Qawlak. Returns the body (or writes it to opts.out); a non-2xx answer raises with the status and body. local function http(cfg, method, path, opts) opts = opts or {} local header, body = TEMP .. SEP .. 'qawlak-auth.txt', TEMP .. SEP .. 'qawlak-body.txt' M.write(header, 'Authorization: Bearer ' .. cfg.key .. '\n') local out = opts.out or body local command = M.system_exe('curl') .. ' -s -S -X ' .. method .. ' -H @' .. M.q(header) .. ' -o ' .. M.q(out) .. ' -w "%{http_code}"' if opts.json then M.write(TEMP .. SEP .. 'qawlak-json.txt', opts.json) command = command .. ' -H "Content-Type: application/json" --data-binary @' .. M.q(TEMP .. SEP .. 'qawlak-json.txt') elseif opts.file then command = command .. ' -H "Content-Type: application/octet-stream" --data-binary @' .. M.q(opts.file) end local ok, result = pcall(M.run, command .. ' ' .. M.q(cfg.url .. path)) M.remove(header) if not ok then error(result, 0) end local code = result:match('(%d%d%d)%s*$') local text = '' if out == body then text = M.read(body) or '' M.remove(body) end if code == '402' then error(M.credit_wall_message(text, cfg.url, cfg.language), 0) end -- P15-43: the credit wall, with the pack link local refusal = code and code:sub(1, 1) == '4' and M.refusal_message and M.refusal_message(text, cfg.language) if refusal then error(refusal, 0) end -- P15-45: e.g. an API key below Studio, with what to use instead if not code or code:sub(1, 1) ~= '2' then error(('%s %s -> %s %s'):format(method, path, tostring(code), text:sub(1, 300)), 0) end return text end local function render_audio(project, timeline, work) local ext = M.render_job(resolve, project, timeline, work, 'qawlak-audio', wait) for _, name in ipairs(M.lines(M.run(M.WINDOWS and ('dir /b ' .. M.q(work .. '\\*.' .. ext)) or ('ls ' .. M.q(work))))) do if name:lower():match('^qawlak%-audio.*%.' .. ext .. '$') then return work .. SEP .. name end end error('The rendered ' .. ext:upper() .. ' was not found under ' .. work, 0) end local function upload(cfg, wav) -- resumable upload in the server's chunk size, offsets chained like resolve_import.Api.upload local size = M.size(wav) if not size then error('Cannot read ' .. wav, 0) end local session = http(cfg, 'POST', '/api/uploads', {json = ('{"name": "%s", "size": %d}'):format(wav:match('([^\\/]+)$'), size)}) local id, chunk = M.field(session, 'id'), tonumber(session:match('"chunk_size"%s*:%s*(%d+)')) local part, offset = TEMP .. SEP .. 'qawlak-chunk.bin', 0 while offset < size do M.write(part, M.read(wav, offset, chunk) or '') offset = tonumber(http(cfg, 'PATCH', '/api/uploads/' .. id .. '?offset=' .. offset, {file = part}):match('"offset"%s*:%s*(%d+)')) end M.remove(part) return http(cfg, 'POST', '/api/uploads/' .. id .. '/complete') end local function transcribe(cfg, pid) http(cfg, 'POST', '/api/projects/' .. pid .. '/transcribe', {json = ('{"language": "%s"}'):format(cfg.language)}) for _ = 1, 1800 do local p = http(cfg, 'GET', '/api/projects/' .. pid) local status = M.field(p, 'status') if status == 'error' then error(M.field(p, 'error') or 'The Qawlak job failed.', 0) end if status == 'ready' then return p end wait(2) end error('Qawlak is still processing; open the project in the studio later.', 0) end local function roundtrip(app, cfg) local project = app:GetProjectManager():GetCurrentProject() if not project then error('No project is open in Resolve.', 0) end local timeline = project:GetCurrentTimeline() if not timeline then error('Open the timeline you want captioned first.', 0) end if cfg.alpha then error('The alpha layer needs a video render, and the round trip sends audio only. Set "alpha": false in resolve.json and export alpha from the Qawlak studio.', 0) end local ping = http(cfg, 'GET', '/api/plugin/ping') -- a bad or revoked key stops here, before any render print(('Qawlak: workspace %s, %s min left'):format(tostring(M.field(ping, 'workspace')), tostring(ping:match('"minutes_left"%s*:%s*([%d%.]+)')))) local low = M.low_balance_message(ping, cfg.url, cfg.language) -- P15-43 if low then print(low) end local work = TEMP .. SEP .. 'qawlak-resolve-' .. os.time() M.run('mkdir ' .. (M.WINDOWS and '' or '-p ') .. M.q(work)) local wav = render_audio(project, timeline, work) print('Qawlak: rendered ' .. wav) local pid = M.field(upload(cfg, wav), 'id') transcribe(cfg, pid) local srt = work .. SEP .. 'captions.srt' http(cfg, 'GET', '/api/projects/' .. pid .. '/export/srt', {out = srt}) local _, message = M.add_subtitles(app, project, timeline, srt, nil, cfg.language) print(message .. ' (project ' .. pid .. ')') end local app = resolve or (Resolve and Resolve()) if not app then return print('Qawlak: run this from Workspace > Scripts inside DaVinci Resolve.') end if CHOICE == 'cancel' then return M.menu_cancel(app) end if not ffi then return M.menu_roundtrip(app, {choice = CHOICE}) end -- the menu: render -> helper -> ImportMedia (see M.menu_roundtrip) local cfg = config() if not cfg.key then return print('Qawlak: no plugin key yet. Create ' .. cfg.path .. ' containing {"key": "qwlk_...", "url": "http://127.0.0.1:8765"} (key from Qawlak > Plugins).') end local ok, err = pcall(roundtrip, app, cfg) if not ok then print('Qawlak: ' .. tostring(err)) end