Write a guided meditation with timed silences
Send a goal (sleep, unwind, focus or a body scan), a length and an optional picture. You get back a script meant to be read aloud: titled sections of spoken lines with a timed silence after most of them. The app then voices it on the listener's device and mixes it over a generated ambient sound; from the API you get the text and the timings.
Basics
Base URL https://api.skillsafe.ai/v1/app-api. Every call except
POST /guest sends Authorization: Bearer <token>; bodies are JSON.
The token is bound to this app, so there is no app header: the slug appears only in the
POST /guest body.
Responses use one envelope: {"ok":true,"data":{...}} on success and
{"ok":false,"error":{"code","message","details"}} on failure.
/estimate, /run and /run-stream take the input object
itself as the body - never wrap it as {"input":{...}}.
Credits: 1 credit = $0.0001. /estimate is free and creates no job; a run holds up
to hold_credits and charges charged_credits when it finishes.
| Status | error.code | Meaning |
|---|---|---|
400 | validation_error | A field of the wrong type, or an object where a scalar belongs; details names it. |
401 | unauthorized | Invalid or expired token. Get a new one (step 1). |
402 | payment_required | The balance is below min_credits. Top up at skillsafe.ai. |
403 | forbidden | A guest token cannot run metered turns unless the app sponsors guests. Sign in for a personal token. |
404 | not_found | Unknown app slug, or a job id that is not yours. |
409 | conflict | The Idempotency-Key was used before with a different body. |
429 | rate_limited | Back off and retry with the same Idempotency-Key. |
5xx | - | Transient. Retry with backoff and the same key. |
Browsers enforce CORS for this API: run the samples from a server, script or terminal.
The task field first
There is one task, meditate. Always send "task": "meditate".
| task | What it does | Reply lines |
|---|---|---|
meditate | Write a meditation script for the goal, length and picture, with timed silences. | title, then 4-7 sections: phase followed by say and pause lines; end last. |
Input fields
Every field is a scalar. The app computes target_words and pause_seconds from the length; send the values below for each length.
| Field | Type | Meaning |
|---|---|---|
task | string | meditate. |
goal | string | sleep (ends by drifting off, no wake-up) | stress (unwind) | focus | bodyscan. |
minutes | number | 5, 10, 15 or 20. |
target_words | number | Spoken words for that length: 360, 590, 790 or 920. |
pause_seconds | number | Total silence for that length: 136, 332, 541 or 782. |
setting | string | breath (no imagery) | forest | shore | nightsky | warmlight. |
posture | string | lying for sleep and body scan, sitting otherwise. |
note | string | Optional: what is on the listener's mind. The app sends at most 600 characters (start and end, the middle replaced by a marker). Empty string when unused. |
change | string | Optional: a change to a previous version ("more silence, no counting"), up to 300 characters. |
retry_note | string | Optional: a format correction after a reply that could not be read; send it with a new attempt number in the key. |
Reply contract
JSON Lines: one object per line with a "k" field, the last line always
{"k":"end"}. A reply without it was cut short - keep the lines that arrived.
Skip any line that is not a JSON object.
| k | Fields |
|---|---|
title | text - 2 to 6 words. |
phase | name: arrive | breath | body | scan | anchor | imagery | release | drift | return; heading. Opens a section; arrive is first, drift (sleep) or return (all other goals) is last. |
say | text - plain sentences to read aloud. |
pause | sec - a whole number of seconds of silence. |
end | - the reply is complete. |
The pause lengths are relative: the app measures how long the voice takes to read each line,
then scales the silences so the whole session lands on minutes. Check the reply
as the app does: spoken words near target_words, no wake-up cue in a sleep
session, and calm, non-clinical wording. This is a relaxation exercise, not medical care.
Step 1 - A tiny client, and a token
A short helper adds the auth header, sends JSON and unwraps the envelope; every later step
reuses it. For the token, open the token page, sign in with
SkillSafe and copy the token. A personal token bills runs to your account. A script with no
browser can mint a guest token with POST /guest and the body
{"slug":"guided-meditation-maker"}, sent with no auth header; guests can
estimate but not run.
export API="https://api.skillsafe.ai/v1/app-api"
export SKILLSAFE_TOKEN="YOUR_TOKEN" # "Copy shell export" on /tokens.html
# No browser? A guest token (this call has no Authorization header):
SKILLSAFE_TOKEN=$(curl -s -X POST "$API/guest" -H "Content-Type: application/json" \
-d '{"slug":"guided-meditation-maker"}' | jq -r '.data.token')
import json, time, requests
API = "https://api.skillsafe.ai/v1/app-api"
TOKEN = "YOUR_TOKEN" # from tokens.html
def api(method, path, body=None, headers=None):
h = {"Content-Type": "application/json", "Authorization": f"Bearer {TOKEN}", **(headers or {})}
res = requests.request(method, API + path, json=body, headers=h, timeout=180)
payload = res.json()
if not res.ok or payload.get("ok") is False:
err = payload.get("error", {})
raise RuntimeError(f"{res.status_code} {err.get('code')}: {err.get('message')}")
return payload["data"]
// Node 18+ (global fetch)
const API = "https://api.skillsafe.ai/v1/app-api";
const TOKEN = "YOUR_TOKEN"; // from tokens.html
async function api(method, path, body, headers = {}) {
const res = await fetch(API + path, {
method,
headers: { "Content-Type": "application/json", Authorization: `Bearer ${TOKEN}`, ...headers },
body: body === undefined ? undefined : JSON.stringify(body),
});
const payload = await res.json();
if (!res.ok || payload.ok === false) {
throw new Error(`${res.status} ${payload.error?.code}: ${payload.error?.message}`);
}
return payload.data;
}
package main
import ("bytes"; "encoding/json"; "fmt"; "net/http"; "time")
const API = "https://api.skillsafe.ai/v1/app-api"
var token = "YOUR_TOKEN" // from tokens.html
func call(method, path string, body any, headers map[string]string, out any) error {
var buf bytes.Buffer
if body != nil { json.NewEncoder(&buf).Encode(body) }
req, _ := http.NewRequest(method, API+path, &buf)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+token)
for k, v := range headers { req.Header.Set(k, v) }
res, err := (&http.Client{Timeout: 180 * time.Second}).Do(req)
if err != nil { return err }
defer res.Body.Close()
var env struct {
Ok bool `json:"ok"`
Data json.RawMessage `json:"data"`
Error struct{ Code, Message string } `json:"error"`
}
json.NewDecoder(res.Body).Decode(&env)
if !env.Ok { return fmt.Errorf("%d %s: %s", res.StatusCode, env.Error.Code, env.Error.Message) }
return json.Unmarshal(env.Data, out)
}
// Java 11+, Jackson for JSON
import com.fasterxml.jackson.databind.*;
import com.fasterxml.jackson.databind.node.*;
import java.net.URI;
import java.net.http.*;
import java.util.Map;
static final String API = "https://api.skillsafe.ai/v1/app-api";
static final String TOKEN = "YOUR_TOKEN"; // from tokens.html
static final ObjectMapper JSON = new ObjectMapper();
static final HttpClient HTTP = HttpClient.newHttpClient();
static JsonNode api(String method, String path, Object body, Map<String, String> headers) throws Exception {
var b = HttpRequest.newBuilder(URI.create(API + path))
.header("Content-Type", "application/json").header("Authorization", "Bearer " + TOKEN)
.method(method, body == null ? HttpRequest.BodyPublishers.noBody()
: HttpRequest.BodyPublishers.ofString(JSON.writeValueAsString(body)));
headers.forEach(b::header);
var res = HTTP.send(b.build(), HttpResponse.BodyHandlers.ofString());
JsonNode env = JSON.readTree(res.body());
if (!env.path("ok").asBoolean()) throw new RuntimeException(res.statusCode() + " " + env.path("error"));
return env.get("data");
}
require "net/http"
require "json"
API = "https://api.skillsafe.ai/v1/app-api"
TOKEN = "YOUR_TOKEN" # from tokens.html
def api(method, path, body = nil, headers = {})
uri = URI(API + path)
req = Net::HTTP.const_get(method.capitalize).new(uri)
req["Content-Type"] = "application/json"
req["Authorization"] = "Bearer #{TOKEN}"
headers.each { |k, v| req[k] = v }
req.body = body.to_json if body
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true, read_timeout: 180) { |h| h.request(req) }
payload = JSON.parse(res.body)
raise "#{res.code} #{payload.dig('error', 'code')}: #{payload.dig('error', 'message')}" unless payload["ok"]
payload["data"]
end
<?php
const API = "https://api.skillsafe.ai/v1/app-api";
const TOKEN = "YOUR_TOKEN"; // from tokens.html
function api(string $method, string $path, ?array $body = null, array $headers = []): array {
$h = ["Content-Type: application/json", "Authorization: Bearer " . TOKEN];
foreach ($headers as $k => $v) $h[] = "$k: $v";
$ch = curl_init(API . $path);
curl_setopt_array($ch, [CURLOPT_CUSTOMREQUEST => $method, CURLOPT_HTTPHEADER => $h,
CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 180]);
if ($body !== null) curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
$payload = json_decode(curl_exec($ch), true);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if (empty($payload["ok"])) throw new RuntimeException("$code " . json_encode($payload["error"] ?? null));
return $payload["data"];
}
using System.Net.Http.Json;
using System.Text.Json;
static class Meditation
{
const string Api = "https://api.skillsafe.ai/v1/app-api";
public static string Token = "YOUR_TOKEN"; // from tokens.html
static readonly HttpClient Http = new() { Timeout = TimeSpan.FromSeconds(180) };
public static async Task<JsonElement> ApiAsync(HttpMethod method, string path, object? body = null,
Dictionary<string, string>? headers = null)
{
var req = new HttpRequestMessage(method, Api + path);
req.Headers.Add("Authorization", "Bearer " + Token);
foreach (var (k, v) in headers ?? new()) req.Headers.Add(k, v);
if (body != null) req.Content = JsonContent.Create(body);
var res = await Http.SendAsync(req);
var env = JsonDocument.Parse(await res.Content.ReadAsStringAsync()).RootElement;
if (!env.GetProperty("ok").GetBoolean()) throw new Exception($"{(int)res.StatusCode} {env.GetProperty("error")}");
return env.GetProperty("data").Clone();
}
}
Step 2 - Who am I, and what is my balance?
subject_type is user for a personal token and guest otherwise; credits is the balance.
curl -s "$API/me" -H "Authorization: Bearer $SKILLSAFE_TOKEN" | jq '.data | {subject_type, credits}'
me = api("GET", "/me")
print(me["subject_type"], me.get("credits"))
const me = await api("GET", "/me");
console.log(me.subject_type, me.credits);
var me struct {
SubjectType string `json:"subject_type"`
Credits int `json:"credits"`
}
if err := call("GET", "/me", nil, nil, &me); err != nil { panic(err) }
fmt.Println(me.SubjectType, me.Credits)
JsonNode me = api("GET", "/me", null, Map.of());
System.out.println(me.get("subject_type") + " " + me.get("credits"));
me = api("GET", "/me")
puts me.values_at("subject_type", "credits").inspect
$me = api("GET", "/me");
echo $me["subject_type"], " ", $me["credits"] ?? 0, PHP_EOL;
var me = await Meditation.ApiAsync(HttpMethod.Get, "/me");
Console.WriteLine($"{me.GetProperty("subject_type")} {me.GetProperty("credits")}");
Step 3 - Price a session for free
/estimate takes the same body as a run and creates no job. It returns
hold_credits (reserved while the run is in flight) and min_credits.
A run is charged charged_credits, usually far less than the hold.
jq -n '{task: "meditate", goal: "stress", minutes: 10, target_words: 590, pause_seconds: 332,
setting: "shore", posture: "sitting", note: "a long week of deadlines"}' > session.json
curl -s -X POST "$API/estimate" \
-H "Authorization: Bearer $SKILLSAFE_TOKEN" -H "Content-Type: application/json" \
-d @session.json | jq '.data | {hold_credits, min_credits, model_alias, warnings}'
session = {"task": "meditate", "goal": "stress", "minutes": 10, "target_words": 590, "pause_seconds": 332,
"setting": "shore", "posture": "sitting", "note": "a long week of deadlines"}
est = api("POST", "/estimate", session)
print(est["hold_credits"], est["min_credits"])
const session = { task: "meditate", goal: "stress", minutes: 10, target_words: 590, pause_seconds: 332,
setting: "shore", posture: "sitting", note: "a long week of deadlines" };
const est = await api("POST", "/estimate", session);
console.log(est.hold_credits, est.min_credits);
session := map[string]any{"task": "meditate", "goal": "stress", "minutes": 10, "target_words": 590,
"pause_seconds": 332, "setting": "shore", "posture": "sitting", "note": "a long week of deadlines"}
var est struct {
Hold int `json:"hold_credits"`
Min int `json:"min_credits"`
}
if err := call("POST", "/estimate", session, nil, &est); err != nil { panic(err) }
fmt.Println(est.Hold, est.Min)
ObjectNode session = JSON.createObjectNode()
.put("task", "meditate").put("goal", "stress").put("minutes", 10).put("target_words", 590)
.put("pause_seconds", 332).put("setting", "shore").put("posture", "sitting")
.put("note", "a long week of deadlines");
JsonNode est = api("POST", "/estimate", session, Map.of());
System.out.println(est.get("hold_credits") + " " + est.get("min_credits"));
session = { task: "meditate", goal: "stress", minutes: 10, target_words: 590, pause_seconds: 332,
setting: "shore", posture: "sitting", note: "a long week of deadlines" }
est = api("POST", "/estimate", session)
puts est.values_at("hold_credits", "min_credits").inspect
$session = ["task" => "meditate", "goal" => "stress", "minutes" => 10, "target_words" => 590,
"pause_seconds" => 332, "setting" => "shore", "posture" => "sitting", "note" => "a long week of deadlines"];
$est = api("POST", "/estimate", $session);
echo $est["hold_credits"], " ", $est["min_credits"], PHP_EOL;
var session = new Dictionary<string, object> {
["task"] = "meditate", ["goal"] = "stress", ["minutes"] = 10, ["target_words"] = 590,
["pause_seconds"] = 332, ["setting"] = "shore", ["posture"] = "sitting", ["note"] = "a long week of deadlines" };
var est = await Meditation.ApiAsync(HttpMethod.Post, "/estimate", session);
Console.WriteLine($"{est.GetProperty("hold_credits")} {est.GetProperty("min_credits")}");
Step 4 - Write the meditation
Send the body with an Idempotency-Key: a retry with the same key and body never
bills twice. Poll the job until it is succeeded or failed, then read
the JSON Lines from output.output. If truncated is true the balance
cut the reply short - keep what arrived.
BODY=$(jq -c . session.json)
KEY="guided-meditation-maker:meditate:$(printf %s "$BODY" | shasum | cut -c1-16):1"
JOB_ID=$(curl -s -X POST "$API/run" \
-H "Authorization: Bearer $SKILLSAFE_TOKEN" -H "Content-Type: application/json" \
-H "Idempotency-Key: $KEY" -d "$BODY" | jq -r '.data.job_id')
while :; do
JOB=$(curl -s "$API/jobs/$JOB_ID" -H "Authorization: Bearer $SKILLSAFE_TOKEN")
STATUS=$(echo "$JOB" | jq -r '.data.status')
[ "$STATUS" = succeeded ] || [ "$STATUS" = failed ] && break
sleep 2
done
echo "$JOB" | jq '.data | {status, charged_credits, truncated}'
echo "$JOB" | jq -r '.data.output.output' | jq -R -c 'fromjson? | select(type == "object")' > script.jsonl
jq -r 'if .k == "say" then .text elif .k == "pause" then " (\(.sec) s)" elif .k == "phase" then "\n== \(.heading)" else empty end' script.jsonl
import hashlib
body = json.dumps(session)
key = "guided-meditation-maker:meditate:" + hashlib.sha1(body.encode()).hexdigest()[:16] + ":1"
job = api("POST", "/run", session, {"Idempotency-Key": key})
while job.get("status") not in ("succeeded", "failed"):
time.sleep(2)
job = api("GET", f"/jobs/{job['job_id']}")
lines = []
for raw in (job.get("output") or {}).get("output", "").splitlines():
try:
lines.append(json.loads(raw))
except ValueError:
pass
for o in lines:
if o.get("k") == "say": print(o["text"])
elif o.get("k") == "pause": print(f" ({o['sec']} s)")
print("complete" if lines and lines[-1].get("k") == "end" else "cut short", job.get("charged_credits"))
import { createHash } from "node:crypto";
const body = JSON.stringify(session);
const key = `guided-meditation-maker:meditate:${createHash("sha1").update(body).digest("hex").slice(0, 16)}:1`;
let job = await api("POST", "/run", session, { "Idempotency-Key": key });
while (!["succeeded", "failed"].includes(job.status)) {
await new Promise((r) => setTimeout(r, 2000));
job = await api("GET", `/jobs/${job.job_id}`);
}
const lines = (job.output?.output || "").split("\n").flatMap((l) => { try { return [JSON.parse(l)]; } catch { return []; } });
for (const o of lines) {
if (o.k === "say") console.log(o.text);
else if (o.k === "pause") console.log(` (${o.sec} s)`);
}
console.log(lines.at(-1)?.k === "end" ? "complete" : "cut short", job.charged_credits);
type Job struct {
JobID string `json:"job_id"`
Status string `json:"status"`
Charged int `json:"charged_credits"`
Output struct{ Output string `json:"output"` } `json:"output"`
}
var job Job
key := "guided-meditation-maker:meditate:example-1" // any stable hash of the exact body
if err := call("POST", "/run", session, map[string]string{"Idempotency-Key": key}, &job); err != nil { panic(err) }
for job.Status != "succeeded" && job.Status != "failed" {
time.Sleep(2 * time.Second)
if err := call("GET", "/jobs/"+job.JobID, nil, nil, &job); err != nil { panic(err) }
}
for _, raw := range strings.Split(job.Output.Output, "\n") { // import "strings"
var o map[string]any
if json.Unmarshal([]byte(raw), &o) != nil { continue }
if o["k"] == "say" { fmt.Println(o["text"]) } else if o["k"] == "pause" { fmt.Printf(" (%v s)\n", o["sec"]) }
}
String key = "guided-meditation-maker:meditate:example-1"; // any stable hash of the exact body
JsonNode job = api("POST", "/run", session, Map.of("Idempotency-Key", key));
while (!job.path("status").asText().matches("succeeded|failed")) {
Thread.sleep(2000);
job = api("GET", "/jobs/" + job.get("job_id").asText(), null, Map.of());
}
for (String raw : job.path("output").path("output").asText().split("\n")) {
try {
JsonNode o = JSON.readTree(raw);
if ("say".equals(o.path("k").asText())) System.out.println(o.get("text").asText());
else if ("pause".equals(o.path("k").asText())) System.out.println(" (" + o.get("sec") + " s)");
} catch (Exception skip) { }
}
require "digest"
key = "guided-meditation-maker:meditate:#{Digest::SHA1.hexdigest(session.to_json)[0, 16]}:1"
job = api("POST", "/run", session, { "Idempotency-Key" => key })
until %w[succeeded failed].include?(job["status"])
sleep 2
job = api("GET", "/jobs/#{job['job_id']}")
end
job.dig("output", "output").to_s.each_line do |raw|
o = JSON.parse(raw) rescue next
puts o["text"] if o["k"] == "say"
puts " (#{o['sec']} s)" if o["k"] == "pause"
end
$key = "guided-meditation-maker:meditate:" . substr(sha1(json_encode($session)), 0, 16) . ":1";
$job = api("POST", "/run", $session, ["Idempotency-Key" => $key]);
while (!in_array($job["status"] ?? "", ["succeeded", "failed"])) {
sleep(2);
$job = api("GET", "/jobs/" . $job["job_id"]);
}
foreach (explode("\n", $job["output"]["output"] ?? "") as $raw) {
$o = json_decode($raw, true);
if (!is_array($o)) continue;
if ($o["k"] === "say") echo $o["text"], PHP_EOL;
elseif ($o["k"] === "pause") echo " (", $o["sec"], " s)", PHP_EOL;
}
var key = "guided-meditation-maker:meditate:example-1"; // any stable hash of the exact body
var job = await Meditation.ApiAsync(HttpMethod.Post, "/run", session, new() { ["Idempotency-Key"] = key });
while (job.GetProperty("status").GetString() is not ("succeeded" or "failed"))
{
await Task.Delay(2000);
job = await Meditation.ApiAsync(HttpMethod.Get, "/jobs/" + job.GetProperty("job_id").GetString());
}
foreach (var raw in job.GetProperty("output").GetProperty("output").GetString()!.Split('\n'))
{
try {
var o = JsonDocument.Parse(raw).RootElement;
var k = o.GetProperty("k").GetString();
if (k == "say") Console.WriteLine(o.GetProperty("text").GetString());
else if (k == "pause") Console.WriteLine($" ({o.GetProperty("sec")} s)");
} catch (JsonException) { }
}
Step 5 - Stream a second take
A rewrite with a change, over Server-Sent Events: a job event,
tick heartbeats, then one done event carrying the whole job. Read
the reply from done. An idempotent replay answers with plain JSON instead of a
stream, so check the content type.
BODY=$(jq -c '. + {change: "more silence, no counting"}' session.json)
KEY="guided-meditation-maker:meditate:$(printf %s "$BODY" | shasum | cut -c1-16):1"
curl -N -s -X POST "$API/run-stream" \
-H "Authorization: Bearer $SKILLSAFE_TOKEN" -H "Content-Type: application/json" \
-H "Idempotency-Key: $KEY" -d "$BODY" > stream.txt
sed -n '/^event: done/{n;s/^data: *//p;}' stream.txt | jq -r '(.job // .) | .output.output' > take2.jsonl
def run_stream(body, key):
h = {"Content-Type": "application/json", "Authorization": f"Bearer {TOKEN}", "Idempotency-Key": key}
res = requests.post(API + "/run-stream", json=body, headers=h, stream=True, timeout=300)
if "text/event-stream" not in res.headers.get("content-type", ""):
return res.json()["data"]
event = None
for line in res.iter_lines(decode_unicode=True):
if line.startswith("event:"):
event = line[6:].strip()
elif line.startswith("data:") and event in ("done", "error"):
data = json.loads(line[5:])
if event == "error":
raise RuntimeError(data)
return data.get("job", data)
raise RuntimeError("stream ended without a done event")
take2 = dict(session, change="more silence, no counting")
done = run_stream(take2, "guided-meditation-maker:meditate:take2:1")
print(done["output"]["output"])
async function runStream(body, key) {
const res = await fetch(API + "/run-stream", {
method: "POST",
headers: { "Content-Type": "application/json", Authorization: `Bearer ${TOKEN}`, "Idempotency-Key": key },
body: JSON.stringify(body),
});
if (!(res.headers.get("content-type") || "").includes("text/event-stream")) return (await res.json()).data;
let event = "";
for (const line of (await res.text()).split("\n")) {
if (line.startsWith("event:")) event = line.slice(6).trim();
else if (line.startsWith("data:") && (event === "done" || event === "error")) {
const data = JSON.parse(line.slice(5));
if (event === "error") throw new Error(JSON.stringify(data));
return data.job || data;
}
}
throw new Error("stream ended without a done event");
}
const done = await runStream({ ...session, change: "more silence, no counting" }, "guided-meditation-maker:meditate:take2:1");
console.log(done.output.output);
// Same request as step 4 against /run-stream; read lines with bufio.Scanner and keep the
// data: line that follows "event: done".
req, _ := http.NewRequest("POST", API+"/run-stream", bytes.NewReader(bodyBytes))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Idempotency-Key", "guided-meditation-maker:meditate:take2:1")
res, _ := http.DefaultClient.Do(req)
sc := bufio.NewScanner(res.Body)
sc.Buffer(make([]byte, 1<<20), 1<<22)
event := ""
for sc.Scan() {
l := sc.Text()
if strings.HasPrefix(l, "event:") { event = strings.TrimSpace(l[6:]) }
if strings.HasPrefix(l, "data:") && event == "done" { fmt.Println(l[5:]); break }
}
var req = HttpRequest.newBuilder(URI.create(API + "/run-stream"))
.header("Content-Type", "application/json").header("Authorization", "Bearer " + TOKEN)
.header("Idempotency-Key", "guided-meditation-maker:meditate:take2:1")
.POST(HttpRequest.BodyPublishers.ofString(JSON.writeValueAsString(session.put("change", "more silence"))))
.build();
String event = "";
for (String line : HTTP.send(req, HttpResponse.BodyHandlers.ofLines()).body().toList()) {
if (line.startsWith("event:")) event = line.substring(6).trim();
else if (line.startsWith("data:") && event.equals("done")) { System.out.println(line.substring(5)); break; }
}
uri = URI(API + "/run-stream")
req = Net::HTTP::Post.new(uri, "Content-Type" => "application/json", "Authorization" => "Bearer #{TOKEN}",
"Idempotency-Key" => "guided-meditation-maker:meditate:take2:1")
req.body = session.merge(change: "more silence, no counting").to_json
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true, read_timeout: 300) { |h| h.request(req) }
event = nil
res.body.each_line do |line|
event = line[6..].strip if line.start_with?("event:")
puts line[5..] and break if line.start_with?("data:") && event == "done"
end
$ch = curl_init(API . "/run-stream");
curl_setopt_array($ch, [CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 300,
CURLOPT_HTTPHEADER => ["Content-Type: application/json", "Authorization: Bearer " . TOKEN,
"Idempotency-Key: guided-meditation-maker:meditate:take2:1"],
CURLOPT_POSTFIELDS => json_encode($session + ["change" => "more silence, no counting"])]);
$event = "";
foreach (explode("\n", curl_exec($ch)) as $line) {
if (str_starts_with($line, "event:")) $event = trim(substr($line, 6));
elseif (str_starts_with($line, "data:") && $event === "done") { echo substr($line, 5), PHP_EOL; break; }
}
session["change"] = "more silence, no counting";
var req = new HttpRequestMessage(HttpMethod.Post, "https://api.skillsafe.ai/v1/app-api/run-stream")
{ Content = JsonContent.Create(session) };
req.Headers.Add("Authorization", "Bearer " + Meditation.Token);
req.Headers.Add("Idempotency-Key", "guided-meditation-maker:meditate:take2:1");
using var http = new HttpClient();
var res = await http.SendAsync(req, HttpCompletionOption.ResponseHeadersRead);
using var reader = new StreamReader(await res.Content.ReadAsStreamAsync());
string? line, ev = "";
while ((line = await reader.ReadLineAsync()) != null)
{
if (line.StartsWith("event:")) ev = line[6..].Trim();
else if (line.StartsWith("data:") && ev == "done") { Console.WriteLine(line[5..]); break; }
}
The browser app does the voice and the ambient sound on the listener's device. To make audio
from the API reply, feed each say line to any text-to-speech engine and insert
the pause silences, scaled so the total lands on minutes.