File size: 6,303 Bytes
7646924
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4f01dd5
7646924
 
 
4f01dd5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7646924
 
 
4f01dd5
 
7646924
 
 
 
 
 
 
 
 
4f01dd5
7646924
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
export const ENDPOINT = 'https://api.valen-team.workers.dev/v1/decisions';
export const MODELS = ['valen-2b', 'valen-4b', 'valen-0-8b', 'valen-preview-0923'];

export function buildPayload({model, mode, question, context = '', options = '', image}) {
  if (!MODELS.includes(model) || !['predicate', 'choice', 'score'].includes(mode)) throw new Error('model');
  question = question.trim(); context = context.trim();
  if (!question || question.length > 1000) throw new Error('question');
  if (context.length > 4000) throw new Error('context');
  const q = {name: 'q0', type: mode, instructions: question};
  if (mode !== 'predicate') {
    const lines = options.split('\n').map(s => s.trim()).filter(Boolean);
    if (lines.length < 2 || lines.length > 8 || lines.some(s => s.length > 300)) throw new Error('options');
    if (mode === 'choice') q.choices = lines.map((description, i) => ({value: `option_${i+1}`, description}));
    else q.levels = lines.map((description, i) => ({label: String(i), description}));
  }
  const evidence = context || (image ? 'Answer using only the visible image.' : question);
  if (image && !/^data:image\/jpeg;base64,[A-Za-z0-9+/]+={0,2}$/.test(image)) throw new Error('image');
  const input = image ? [{role: 'user', content: [{type: 'input_text', text: evidence}, {type: 'input_image', image_url: image}]}] : evidence;
  const payload = {model, input, questions: [q], temperature: 1.0};
  if (new TextEncoder().encode(JSON.stringify(payload)).length > 4500000) throw new Error('size');
  return payload;
}

export function previewPayload(payload) {
  const value = structuredClone(payload);
  if (Array.isArray(value.input)) value.input[0].content[1].image_url = '<JPEG data URL omitted from preview>';
  return value;
}

export function pythonExample(payload, attachment) {
  const value = structuredClone(payload);
  const hasImage = Array.isArray(value.input);
  if (hasImage) value.input[0].content[1].image_url = 'IMAGE_DATA_URL';
  const isVideo=hasImage&&attachment?.kind==='video';
  const filename=JSON.stringify(attachment?.name||(isVideo?'video.mp4':'image.jpg'));
  const setup = isVideo ? `import base64
import io
import av

# Extract the middle frame, as in the Valen cookbook.
video_path = ${filename}
with av.open(video_path) as video:
    frame_count = sum(1 for _ in video.decode(video=0))
if frame_count == 0:
    raise ValueError("The video has no decodable frames")
with av.open(video_path) as video:
    for index, frame in enumerate(video.decode(video=0)):
        if index == frame_count // 2:
            image = frame.to_image().convert("RGB")
            break
image.thumbnail((1536, 1536))
buffer = io.BytesIO()
image.save(buffer, format="JPEG", quality=85)
image_url = "data:image/jpeg;base64," + base64.b64encode(buffer.getvalue()).decode("ascii")

` : hasImage ? `import base64
import io
from PIL import Image, ImageOps

# Use your selected image in the current directory.
with Image.open(${filename}) as image:
    image = ImageOps.exif_transpose(image).convert("RGB")
    image.thumbnail((1536, 1536))
    buffer = io.BytesIO()
    image.save(buffer, format="JPEG", quality=85)
image_url = "data:image/jpeg;base64," + base64.b64encode(buffer.getvalue()).decode("ascii")

` : '';
  // json.loads safely preserves quotes, Unicode and literal user text in Python.
  const literal = JSON.stringify(JSON.stringify(value)).replaceAll('\u2028', '\\u2028').replaceAll('\u2029', '\\u2029');
  return `# pip install requests${hasImage ? ' Pillow' : ''}${isVideo ? ' av==16.1.0' : ''}
import json
import requests
${setup}
payload = json.loads(${literal})
${hasImage ? 'payload["input"][0]["content"][1]["image_url"] = image_url\n' : ''}
response = requests.post(
    "${ENDPOINT}",
    headers={"Origin": "https://valen-team.github.io"},
    json=payload,
    timeout=120,
)
response.raise_for_status()
result = response.json()
print(result["answers"][0])
print("Model ID:", result.get("model_id"))
`;
}

export function parseAnswer(data, payload) {
  const fail = () => {throw new Error('response');};
  const q = payload.questions[0];
  if (!Array.isArray(data?.answers) || data.answers.length !== 1) fail();
  const a = data.answers[0];
  const probability = value => typeof value === 'number' && Number.isFinite(value) && value >= 0 && value <= 1;
  if (!a || a.name !== 'q0' || a.type !== q.type || (a.confidence !== undefined && !probability(a.confidence))) fail();
  if (q.type === 'predicate') {
    if (!probability(a.probability)) fail();
    return {title: a.probability >= .5 ? 'yes' : 'no', confidence: a.confidence,
      rows: [{label: 'yes', probability: a.probability}, {label: 'no', probability: 1 - a.probability}]};
  }
  const labels = q.type === 'choice' ? new Map(q.choices.map(c => [c.value, c.description])) : new Map(q.levels.map((l, i) => [i, l.description]));
  if (!Array.isArray(a.probabilities) || a.probabilities.length !== labels.size) fail();
  const seen = new Set();
  const rows = a.probabilities.map(p => {
    if (!p) fail();
    const key = q.type === 'score' && typeof p.value === 'string' && /^\d+$/.test(p.value) ? Number(p.value) : p.value;
    if (!labels.has(key) || seen.has(key) || !probability(p.probability)) fail();
    seen.add(key);
    return {label: labels.get(key), probability: p.probability};
  });
  if (Math.abs(rows.reduce((sum, row) => sum + row.probability, 0) - 1) > .02) fail();
  let title;
  if (q.type === 'choice') {if (!labels.has(a.choice)) fail(); title = labels.get(a.choice);}
  else {
    if (typeof a.score !== 'number' || !Number.isFinite(a.score) || a.score < 0 || a.score > labels.size-1) fail();
    title = `${a.score.toFixed(2)} / ${labels.size-1}`;
  }
  return {title, rows, confidence: a.confidence};
}

export async function requestDecision(payload, signal, fetcher = fetch) {
  // The browser supplies the Space's real Origin. Never try to spoof it in JS.
  const response = await fetcher(ENDPOINT, {method: 'POST', headers: {'Content-Type': 'application/json'},
    body: JSON.stringify(payload), signal, credentials: 'omit', redirect: 'error'});
  if (!response.ok) throw new Error(`http_${response.status}`);
  let data;
  try {data = await response.json();} catch {throw new Error('response');}
  return {data, answer: parseAnswer(data, payload)};
}