yuhangzang commited on
Commit
4f01dd5
·
verified ·
1 Parent(s): 7646924

Use website text, image and video cookbook examples

Browse files
Files changed (12) hide show
  1. .gitattributes +1 -0
  2. NOTICE.md +9 -0
  3. README.md +13 -3
  4. api.mjs +27 -5
  5. app.mjs +36 -27
  6. cookbook/momos-frame.jpg +0 -0
  7. cookbook/momos.mp4 +3 -0
  8. cookbook/sheep.jpg +0 -0
  9. index.html +4 -4
  10. locales.mjs +19 -10
  11. media.mjs +42 -0
  12. style.css +2 -0
.gitattributes CHANGED
@@ -34,3 +34,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
  assets/valen-mark.png filter=lfs diff=lfs merge=lfs -text
 
 
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
  assets/valen-mark.png filter=lfs diff=lfs merge=lfs -text
37
+ cookbook/momos.mp4 filter=lfs diff=lfs merge=lfs -text
NOTICE.md CHANGED
@@ -10,3 +10,12 @@ This Space uses a subset of the Valen shared design system, version 0.5.1.
10
  Valen branding identifies Valen Team and its projects. No trademark rights are
11
  granted. No additional open-source license is asserted for the application or
12
  design source. Snapshot hashes are recorded in `assets/manifest.json`.
 
 
 
 
 
 
 
 
 
 
10
  Valen branding identifies Valen Team and its projects. No trademark rights are
11
  granted. No additional open-source license is asserted for the application or
12
  design source. Snapshot hashes are recorded in `assets/manifest.json`.
13
+
14
+ ## Cookbook examples
15
+
16
+ The text, image and video examples are adapted from the Valen website's
17
+ `src/components/modern/cookbook.ts` and the official
18
+ [API cookbook](https://github.com/Valen-Team/Valen/blob/main/cookbook/api_quickstart.ipynb).
19
+ `cookbook/sheep.jpg`, `cookbook/momos.mp4` and `cookbook/momos-frame.jpg` are copied
20
+ unchanged from `valen-team.github.io/static/cookbook/`. They retain their upstream
21
+ rights and attribution. Video preprocessing is adapted from the website's `media.ts`.
README.md CHANGED
@@ -6,13 +6,13 @@ colorTo: gray
6
  sdk: static
7
  app_file: index.html
8
  fullWidth: true
9
- short_description: Valen decisions with text, images and API examples
10
  ---
11
 
12
  # Valen API
13
 
14
  A bilingual, static playground for the [Valen Decisions API](https://valen-team.github.io/docs/valen/api/).
15
- Use text or images with native **predicate**, **choice**, and **score** questions.
16
  Results show probability bars, optional confidence, the returned model ID, and raw JSON.
17
  The Python example follows the current inputs, so you can reproduce a request in your own application.
18
 
@@ -65,7 +65,16 @@ browser flow on the deployed Space. `?lang=zh` opens the Chinese interface.
65
  Questions are limited to 1000 characters, context to 4000, each option to 300.
66
  JPG/PNG/WebP uploads are limited to 3 MB. Images are resized locally to fit
67
  1536 × 1536 and encoded as JPEG before submission. The request preview omits the
68
- base64 bytes; the generated Python example reads a local `image.jpg` instead.
 
 
 
 
 
 
 
 
 
69
 
70
  ## Data
71
 
@@ -79,6 +88,7 @@ Requests can be cancelled and time out after two minutes.
79
 
80
  - `index.html`, `style.css`, `app.mjs`, `locales.mjs`: responsive bilingual UI.
81
  - `api.mjs`: request validation, response parsing and Python examples.
 
82
  - `tests/`: offline contract, error and example-generation tests.
83
  - `assets/`: pinned Valen design tokens, brand mark and font; see `NOTICE.md`.
84
 
 
6
  sdk: static
7
  app_file: index.html
8
  fullWidth: true
9
+ short_description: Valen API examples with text, images and video
10
  ---
11
 
12
  # Valen API
13
 
14
  A bilingual, static playground for the [Valen Decisions API](https://valen-team.github.io/docs/valen/api/).
15
+ Use text, images, or a video frame with native **predicate**, **choice**, and **score** questions.
16
  Results show probability bars, optional confidence, the returned model ID, and raw JSON.
17
  The Python example follows the current inputs, so you can reproduce a request in your own application.
18
 
 
65
  Questions are limited to 1000 characters, context to 4000, each option to 300.
66
  JPG/PNG/WebP uploads are limited to 3 MB. Images are resized locally to fit
67
  1536 × 1536 and encoded as JPEG before submission. The request preview omits the
68
+ base64 bytes; the generated Python example reads the selected local filename.
69
+ MP4/WebM videos up to 20 MB are decoded in the browser, and only a JPEG of the
70
+ middle frame is sent as `input_image`. This is not whole-video inference; a single
71
+ frame cannot establish motion or event order. The Python video example uses PyAV
72
+ to extract a middle frame locally.
73
+
74
+ The three examples match the website cookbook: a customer changing a delivery
75
+ address (text), naming a group of sheep (image), and identifying the sauce color
76
+ in `momos.mp4` (video). Example media is bundled in `cookbook/`. Clicking an
77
+ example loads its inputs and prepares the image/frame without calling the API.
78
 
79
  ## Data
80
 
 
88
 
89
  - `index.html`, `style.css`, `app.mjs`, `locales.mjs`: responsive bilingual UI.
90
  - `api.mjs`: request validation, response parsing and Python examples.
91
+ - `media.mjs`: local image/video decoding and frame extraction.
92
  - `tests/`: offline contract, error and example-generation tests.
93
  - `assets/`: pinned Valen design tokens, brand mark and font; see `NOTICE.md`.
94
 
api.mjs CHANGED
@@ -27,16 +27,38 @@ export function previewPayload(payload) {
27
  return value;
28
  }
29
 
30
- export function pythonExample(payload) {
31
  const value = structuredClone(payload);
32
  const hasImage = Array.isArray(value.input);
33
  if (hasImage) value.input[0].content[1].image_url = 'IMAGE_DATA_URL';
34
- const setup = hasImage ? `import base64
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
35
  import io
36
  from PIL import Image, ImageOps
37
 
38
- # Replace image.jpg with your selected image.
39
- with Image.open("image.jpg") as image:
40
  image = ImageOps.exif_transpose(image).convert("RGB")
41
  image.thumbnail((1536, 1536))
42
  buffer = io.BytesIO()
@@ -46,7 +68,7 @@ image_url = "data:image/jpeg;base64," + base64.b64encode(buffer.getvalue()).deco
46
  ` : '';
47
  // json.loads safely preserves quotes, Unicode and literal user text in Python.
48
  const literal = JSON.stringify(JSON.stringify(value)).replaceAll('\u2028', '\\u2028').replaceAll('\u2029', '\\u2029');
49
- return `# pip install requests${hasImage ? ' Pillow' : ''}
50
  import json
51
  import requests
52
  ${setup}
 
27
  return value;
28
  }
29
 
30
+ export function pythonExample(payload, attachment) {
31
  const value = structuredClone(payload);
32
  const hasImage = Array.isArray(value.input);
33
  if (hasImage) value.input[0].content[1].image_url = 'IMAGE_DATA_URL';
34
+ const isVideo=hasImage&&attachment?.kind==='video';
35
+ const filename=JSON.stringify(attachment?.name||(isVideo?'video.mp4':'image.jpg'));
36
+ const setup = isVideo ? `import base64
37
+ import io
38
+ import av
39
+
40
+ # Extract the middle frame, as in the Valen cookbook.
41
+ video_path = ${filename}
42
+ with av.open(video_path) as video:
43
+ frame_count = sum(1 for _ in video.decode(video=0))
44
+ if frame_count == 0:
45
+ raise ValueError("The video has no decodable frames")
46
+ with av.open(video_path) as video:
47
+ for index, frame in enumerate(video.decode(video=0)):
48
+ if index == frame_count // 2:
49
+ image = frame.to_image().convert("RGB")
50
+ break
51
+ image.thumbnail((1536, 1536))
52
+ buffer = io.BytesIO()
53
+ image.save(buffer, format="JPEG", quality=85)
54
+ image_url = "data:image/jpeg;base64," + base64.b64encode(buffer.getvalue()).decode("ascii")
55
+
56
+ ` : hasImage ? `import base64
57
  import io
58
  from PIL import Image, ImageOps
59
 
60
+ # Use your selected image in the current directory.
61
+ with Image.open(${filename}) as image:
62
  image = ImageOps.exif_transpose(image).convert("RGB")
63
  image.thumbnail((1536, 1536))
64
  buffer = io.BytesIO()
 
68
  ` : '';
69
  // json.loads safely preserves quotes, Unicode and literal user text in Python.
70
  const literal = JSON.stringify(JSON.stringify(value)).replaceAll('\u2028', '\\u2028').replaceAll('\u2029', '\\u2029');
71
+ return `# pip install requests${hasImage ? ' Pillow' : ''}${isVideo ? ' av==16.1.0' : ''}
72
  import json
73
  import requests
74
  ${setup}
app.mjs CHANGED
@@ -1,8 +1,9 @@
1
  import {buildPayload, previewPayload, pythonExample, requestDecision} from './api.mjs';
2
- import {words, examples} from './locales.mjs';
 
3
  const $ = id => document.getElementById(id);
4
  let language = new URL(location.href).searchParams.get('lang') === 'zh' ? 'zh' : 'en';
5
- let imageData, imageTask = 0, controller, resultData, requestPayload, lastError, state = 'ready', statusKey = '';
6
  const t = key => words[language][key] || key;
7
 
8
  function renderLanguage() {
@@ -36,13 +37,14 @@ function showError(error) {
36
  renderState();
37
  }
38
  function currentPayload() {
 
39
  return buildPayload({model:$('model').value,mode:$('mode').value,question:$('question').value,
40
  context:$('context').value,options:$('options').value,image:imageData});
41
  }
42
  function updatePreview(report = false) {
43
  try {
44
  const payload = currentPayload();
45
- $('python-code').textContent = pythonExample(payload);
46
  $('request-json').textContent = JSON.stringify(previewPayload(payload),null,2);
47
  $('copy-code').disabled = false;return payload;
48
  } catch (error) {
@@ -75,32 +77,38 @@ function lock(value) {
75
  $('preview-code').disabled=value;
76
  }
77
  function removeImage() {
78
- imageTask++;imageData=undefined;$('image').value='';
79
- $('thumbnail').removeAttribute('src');$('image-preview').hidden=true;clearResult();updatePreview();
 
 
 
80
  }
81
- $('image').addEventListener('change', async () => {
82
- const file = $('image').files[0];if (!file) {removeImage();return;}
83
- const task=++imageTask;imageData=undefined;clearResult();$('image-preview').hidden=true;
84
- lock(true);state='busy';statusKey='reading';renderState();let url;
85
  try {
86
- if (!['image/jpeg','image/png','image/webp'].includes(file.type)) throw new Error('image');
87
- if (file.size > 3000000) throw new Error('size');
88
- url=URL.createObjectURL(file);const image=new Image();image.src=url;await image.decode();
89
- if (!image.naturalWidth || !image.naturalHeight) throw new Error('image');
90
- const factor=Math.min(1,1536/image.naturalWidth,1536/image.naturalHeight);
91
- const canvas=document.createElement('canvas');canvas.width=Math.max(1,Math.round(image.naturalWidth*factor));canvas.height=Math.max(1,Math.round(image.naturalHeight*factor));
92
- const ctx=canvas.getContext('2d');ctx.fillStyle='#fff';ctx.fillRect(0,0,canvas.width,canvas.height);ctx.drawImage(image,0,0,canvas.width,canvas.height);
93
- if(task!==imageTask)return;
94
- imageData=canvas.toDataURL('image/jpeg',.85);
95
- $('thumbnail').src=imageData;$('image-name').textContent=file.name;$('image-preview').hidden=false;
96
  state='ready';statusKey='';renderState();updatePreview();
97
  } catch(error) {
98
- if(task!==imageTask)return;
99
- $('image').value='';showError(new Error(error.message==='size'?'size':'image'));
100
- } finally {if(url)URL.revokeObjectURL(url);if(task===imageTask)lock(false);}
 
 
 
101
  });
102
  $('decision-form').addEventListener('submit', async event => {
103
- event.preventDefault();if(controller)return;
104
  clearResult();const payload=updatePreview(true);if(!payload)return;
105
  requestPayload=payload;controller=new AbortController();const active=controller;
106
  const timeout=setTimeout(()=>active.abort('timeout'),120000);
@@ -120,16 +128,17 @@ $('remove-image').addEventListener('click',removeImage);
120
  $('language').addEventListener('click',()=>{language=language==='en'?'zh':'en';renderLanguage();updatePreview();});
121
  $('theme').addEventListener('click',()=>{document.body.dataset.theme=document.body.dataset.theme==='dark'?'light':'dark';});
122
  $('mode').addEventListener('change',()=>{
123
- if($('mode').value==='score'&&!$('options').value.trim())$('options').value=examples[language][2][3];configureOptions();
124
  });
125
  for (const name of ['question','context','options','model','mode']) {
126
  $(name).addEventListener('input',()=>{clearResult();updatePreview();});
127
  $(name).addEventListener('change',()=>{configureOptions();updatePreview();});
128
  }
129
- document.querySelectorAll('[data-example]').forEach(button=>button.addEventListener('click',()=>{
130
- removeImage();const example=examples[language][Number(button.dataset.example)];
131
  ['mode','question','context','options'].forEach((id,index)=>{$(id).value=example[index];});
132
- configureOptions();updatePreview();$('question').focus({preventScroll:true});
 
133
  $('decision-form').scrollIntoView({behavior:matchMedia('(prefers-reduced-motion: reduce)').matches?'instant':'smooth',block:'center'});
134
  }));
135
  $('preview-code').addEventListener('click',()=>{updatePreview(true);});
 
1
  import {buildPayload, previewPayload, pythonExample, requestDecision} from './api.mjs';
2
+ import {words, examples, exampleMedia, ratingOptions} from './locales.mjs';
3
+ import {readMedia} from './media.mjs';
4
  const $ = id => document.getElementById(id);
5
  let language = new URL(location.href).searchParams.get('lang') === 'zh' ? 'zh' : 'en';
6
+ let imageData, attachment, mediaController, mediaRequired = false, controller, resultData, requestPayload, lastError, state = 'ready', statusKey = '';
7
  const t = key => words[language][key] || key;
8
 
9
  function renderLanguage() {
 
37
  renderState();
38
  }
39
  function currentPayload() {
40
+ if(mediaRequired&&!imageData)throw new Error('image');
41
  return buildPayload({model:$('model').value,mode:$('mode').value,question:$('question').value,
42
  context:$('context').value,options:$('options').value,image:imageData});
43
  }
44
  function updatePreview(report = false) {
45
  try {
46
  const payload = currentPayload();
47
+ $('python-code').textContent = pythonExample(payload,attachment);
48
  $('request-json').textContent = JSON.stringify(previewPayload(payload),null,2);
49
  $('copy-code').disabled = false;return payload;
50
  } catch (error) {
 
77
  $('preview-code').disabled=value;
78
  }
79
  function removeImage() {
80
+ mediaController?.abort();mediaController=null;
81
+ $('video-preview').pause();$('video-preview').removeAttribute('src');$('video-preview').load();$('video-preview').hidden=true;
82
+ if(attachment?.objectUrl)URL.revokeObjectURL(attachment.objectUrl);
83
+ attachment=undefined;imageData=undefined;mediaRequired=false;$('image').value='';
84
+ $('thumbnail').removeAttribute('src');$('image-preview').hidden=true;$('video-note').hidden=true;clearResult();updatePreview();
85
  }
86
+ async function loadMedia(fileOrExample) {
87
+ removeImage();mediaRequired=true;
88
+ const active=new AbortController();mediaController=active;
89
+ lock(true);state='busy';statusKey='reading';renderState();
90
  try {
91
+ let file=fileOrExample;
92
+ if(!(file instanceof File)) {
93
+ const response=await fetch(file.src,{signal:active.signal});if(!response.ok)throw new Error('image');
94
+ file=new File([await response.blob()],file.name,{type:file.kind==='video'?'video/mp4':'image/jpeg'});
95
+ }
96
+ const next=await readMedia(file,active.signal);
97
+ if(active.signal.aborted){URL.revokeObjectURL(next.objectUrl);return;}
98
+ attachment=next;imageData=next.image;
99
+ $('thumbnail').src=imageData;$('image-name').textContent=next.name;$('image-preview').hidden=false;
100
+ if(next.kind==='video'){$('video-preview').src=next.video;$('video-preview').poster=imageData;$('video-preview').hidden=false;$('video-note').hidden=false;}
101
  state='ready';statusKey='';renderState();updatePreview();
102
  } catch(error) {
103
+ if(active.signal.aborted)return;
104
+ $('image').value='';showError(new Error(error.message==='size'?'size':'image'));updatePreview();
105
+ } finally {if(mediaController===active){mediaController=null;lock(false);}}
106
+ }
107
+ $('image').addEventListener('change',()=>{
108
+ const file=$('image').files[0];if(!file){removeImage();return;}void loadMedia(file);
109
  });
110
  $('decision-form').addEventListener('submit', async event => {
111
+ event.preventDefault();if(controller||mediaController)return;
112
  clearResult();const payload=updatePreview(true);if(!payload)return;
113
  requestPayload=payload;controller=new AbortController();const active=controller;
114
  const timeout=setTimeout(()=>active.abort('timeout'),120000);
 
128
  $('language').addEventListener('click',()=>{language=language==='en'?'zh':'en';renderLanguage();updatePreview();});
129
  $('theme').addEventListener('click',()=>{document.body.dataset.theme=document.body.dataset.theme==='dark'?'light':'dark';});
130
  $('mode').addEventListener('change',()=>{
131
+ if($('mode').value==='score'&&!$('options').value.trim())$('options').value=ratingOptions[language];configureOptions();
132
  });
133
  for (const name of ['question','context','options','model','mode']) {
134
  $(name).addEventListener('input',()=>{clearResult();updatePreview();});
135
  $(name).addEventListener('change',()=>{configureOptions();updatePreview();});
136
  }
137
+ document.querySelectorAll('[data-example]').forEach(button=>button.addEventListener('click',async()=>{
138
+ removeImage();const index=Number(button.dataset.example);const example=examples[language][index];
139
  ['mode','question','context','options'].forEach((id,index)=>{$(id).value=example[index];});
140
+ configureOptions();if(exampleMedia[index])await loadMedia(exampleMedia[index]);else updatePreview();
141
+ $('question').focus({preventScroll:true});
142
  $('decision-form').scrollIntoView({behavior:matchMedia('(prefers-reduced-motion: reduce)').matches?'instant':'smooth',block:'center'});
143
  }));
144
  $('preview-code').addEventListener('click',()=>{updatePreview(true);});
cookbook/momos-frame.jpg ADDED
cookbook/momos.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:1b51c243276e7016ddce3643891400570a079660ccf02b3bac7afe94827e2e6a
3
+ size 395352
cookbook/sheep.jpg ADDED
index.html CHANGED
@@ -3,7 +3,7 @@
3
  <head>
4
  <meta charset="utf-8">
5
  <meta name="viewport" content="width=device-width,initial-scale=1">
6
- <meta name="description" content="Try Valen's free decision API with text and images. Yes/no, choices, and ratings with ready-to-run Python examples.">
7
  <meta name="color-scheme" content="light dark">
8
  <title>Valen API</title>
9
  <link rel="icon" href="./assets/valen-mark.png">
@@ -13,7 +13,7 @@
13
  <body>
14
  <header class="nav"><a class="brand" href="https://valen-team.github.io/" target="_blank" rel="noreferrer"><img src="./assets/valen-mark.png" alt=""><strong>valen</strong><span>API</span></a><nav aria-label="Navigation"><a href="https://valen-team.github.io/docs/valen/api/" target="_blank" rel="noreferrer" data-i18n="docs">Documentation ↗</a><button id="language" type="button" aria-label="Switch language">中文</button><button id="theme" type="button" aria-label="Switch color theme">◐</button></nav></header>
15
  <main>
16
- <section class="intro"><span class="eyebrow" data-i18n="eyebrow">THE VALEN API PLAYGROUND</span><h1 data-i18n="title">A question. A clear decision.</h1><p data-i18n="subtitle">Bring text or an image. Get probabilities for what comes next.</p><div class="intro-meta"><span class="dot"></span><span data-i18n="free">Free API · No key needed</span><span class="divider">/</span><span data-i18n="formats">Yes / No · Choices · Rating</span></div></section>
17
  <div class="workspace">
18
  <section class="panel input-panel"><div class="panel-heading"><span class="step">01</span><h2 data-i18n="input">Your input</h2></div>
19
  <form id="decision-form"><fieldset id="fields">
@@ -21,7 +21,7 @@
21
  <label><span data-i18n="question">Question</span><textarea id="question" maxlength="1000" rows="2" required data-placeholder="questionPlaceholder" placeholder="What would you like Valen to decide?"></textarea></label>
22
  <label><span data-i18n="context">Context <small>optional</small></span><textarea id="context" maxlength="4000" rows="3" data-placeholder="contextPlaceholder" placeholder="Paste the text to assess, or add some context…"></textarea></label>
23
  <label id="options-field" hidden><span id="options-label">Choices · one per line</span><textarea id="options" rows="4" placeholder="Option A&#10;Option B"></textarea><small data-i18n="optionsHint">2–8 entries, up to 300 characters each.</small></label>
24
- <div class="attachment"><label class="upload-label" for="image"><span class="upload-icon">+</span><span><strong data-i18n="attach">Add an image</strong><small data-i18n="imageHint">Optional · JPG, PNG, WebP · up to 3 MB</small></span></label><input id="image" type="file" accept="image/jpeg,image/png,image/webp"><div id="image-preview" hidden><img id="thumbnail" alt="Selected image"><span id="image-name"></span><button id="remove-image" type="button" data-i18n="remove">Remove</button></div></div>
25
  <button id="submit" class="primary" type="submit"><span data-i18n="submit">Ask Valen</span><span aria-hidden="true">↗</span></button>
26
  </fieldset></form>
27
  <button id="cancel" class="cancel" type="button" hidden data-i18n="cancel">Cancel request</button>
@@ -34,7 +34,7 @@
34
  <div class="api-note"><span class="mini-label">POST</span><code>/v1/decisions</code><p data-i18n="quota">Free capacity is limited. On HTTP 429, wait at least one minute before retrying.</p></div>
35
  </section>
36
  </div>
37
- <section class="examples"><div class="section-heading"><h2 data-i18n="examplesTitle">Start with a question</h2><span data-i18n="examplesNote">Loads an example. You choose when to send.</span></div><div class="example-grid"><button type="button" data-example="0"><span data-i18n="example0Tag">YES / NO</span><strong data-i18n="example0Title">A happy customer?</strong><p data-i18n="example0Text">Find the sentiment behind a message.</p><i aria-hidden="true">↗</i></button><button type="button" data-example="1"><span data-i18n="example1Tag">CHOICES</span><strong data-i18n="example1Title">What do they need?</strong><p data-i18n="example1Text">Choose the right next step for support.</p><i aria-hidden="true">↗</i></button><button type="button" data-example="2"><span data-i18n="example2Tag">RATING</span><strong data-i18n="example2Title">Ready to buy?</strong><p data-i18n="example2Text">Rate purchase intent on five levels.</p><i aria-hidden="true">↗</i></button></div></section>
38
  <section class="code-section"><details id="code-details"><summary><span data-i18n="codeTitle">Make it your own · API example</span><span class="code-tag">PYTHON</span></summary><p data-i18n="codeNote">Code follows your current input. Generating a preview does not call the API.</p><div class="code-actions"><button id="preview-code" type="button" data-i18n="preview">Update preview</button><button id="copy-code" type="button" data-i18n="copy">Copy Python</button><span id="copy-status" role="status"></span></div><pre><code id="python-code"></code></pre><details><summary data-i18n="requestPreview">Request JSON · image data omitted</summary><pre id="request-json"></pre></details></details></section>
39
  <footer><span>© 2026 Valen Team / 万澜</span><p data-i18n="network">Your browser connects directly to api.valen-team.workers.dev. This Space uses the same API as the website.</p><a href="https://huggingface.co/spaces/Valen-Team/Valen-API/tree/main" target="_blank" rel="noreferrer" data-i18n="source">View source ↗</a></footer>
40
  </main>
 
3
  <head>
4
  <meta charset="utf-8">
5
  <meta name="viewport" content="width=device-width,initial-scale=1">
6
+ <meta name="description" content="Try Valen's free decision API with text, images and videos. Yes/no, choices, and ratings with ready-to-run Python examples.">
7
  <meta name="color-scheme" content="light dark">
8
  <title>Valen API</title>
9
  <link rel="icon" href="./assets/valen-mark.png">
 
13
  <body>
14
  <header class="nav"><a class="brand" href="https://valen-team.github.io/" target="_blank" rel="noreferrer"><img src="./assets/valen-mark.png" alt=""><strong>valen</strong><span>API</span></a><nav aria-label="Navigation"><a href="https://valen-team.github.io/docs/valen/api/" target="_blank" rel="noreferrer" data-i18n="docs">Documentation ↗</a><button id="language" type="button" aria-label="Switch language">中文</button><button id="theme" type="button" aria-label="Switch color theme">◐</button></nav></header>
15
  <main>
16
+ <section class="intro"><span class="eyebrow" data-i18n="eyebrow">THE VALEN API PLAYGROUND</span><h1 data-i18n="title">A question. A clear decision.</h1><p data-i18n="subtitle">Bring text, an image, or a video. Get probabilities for what comes next.</p><div class="intro-meta"><span class="dot"></span><span data-i18n="free">Free API · No key needed</span><span class="divider">/</span><span data-i18n="formats">Yes / No · Choices · Rating</span></div></section>
17
  <div class="workspace">
18
  <section class="panel input-panel"><div class="panel-heading"><span class="step">01</span><h2 data-i18n="input">Your input</h2></div>
19
  <form id="decision-form"><fieldset id="fields">
 
21
  <label><span data-i18n="question">Question</span><textarea id="question" maxlength="1000" rows="2" required data-placeholder="questionPlaceholder" placeholder="What would you like Valen to decide?"></textarea></label>
22
  <label><span data-i18n="context">Context <small>optional</small></span><textarea id="context" maxlength="4000" rows="3" data-placeholder="contextPlaceholder" placeholder="Paste the text to assess, or add some context…"></textarea></label>
23
  <label id="options-field" hidden><span id="options-label">Choices · one per line</span><textarea id="options" rows="4" placeholder="Option A&#10;Option B"></textarea><small data-i18n="optionsHint">2–8 entries, up to 300 characters each.</small></label>
24
+ <div class="attachment"><label class="upload-label" for="image"><span class="upload-icon">+</span><span><strong data-i18n="attach">Add an image or video</strong><small data-i18n="imageHint">Images up to 3 MB · MP4 / WebM up to 20 MB</small></span></label><input id="image" type="file" accept="image/jpeg,image/png,image/webp,video/mp4,video/webm"><div id="image-preview" hidden><img id="thumbnail" alt="Selected image"><span id="image-name"></span><button id="remove-image" type="button" data-i18n="remove">Remove</button></div><video id="video-preview" controls playsinline preload="metadata" hidden></video><p id="video-note" class="note" data-i18n="videoNote" hidden>Only the middle frame is sent to the model.</p></div>
25
  <button id="submit" class="primary" type="submit"><span data-i18n="submit">Ask Valen</span><span aria-hidden="true">↗</span></button>
26
  </fieldset></form>
27
  <button id="cancel" class="cancel" type="button" hidden data-i18n="cancel">Cancel request</button>
 
34
  <div class="api-note"><span class="mini-label">POST</span><code>/v1/decisions</code><p data-i18n="quota">Free capacity is limited. On HTTP 429, wait at least one minute before retrying.</p></div>
35
  </section>
36
  </div>
37
+ <section class="examples"><div class="section-heading"><h2 data-i18n="examplesTitle">Start with a question</h2><span data-i18n="examplesNote">Loads an example. You choose when to send.</span></div><div class="example-grid"><button type="button" data-example="0"><span data-i18n="example0Tag">TEXT</span><strong data-i18n="example0Title">What does this customer need?</strong><p data-i18n="example0Text">Read a message and identify its intent.</p><i aria-hidden="true">↗</i></button><button type="button" data-example="1"><span data-i18n="example1Tag">IMAGE</span><img class="example-image" src="./cookbook/sheep.jpg" alt="" loading="lazy"><strong data-i18n="example1Title">What is this group called?</strong><p data-i18n="example1Text">Look at the animals and choose a name.</p><i aria-hidden="true">↗</i></button><button type="button" data-example="2"><span data-i18n="example2Tag">VIDEO</span><img class="example-image" src="./cookbook/momos-frame.jpg" alt="" loading="lazy"><strong data-i18n="example2Title">What color is the sauce?</strong><p data-i18n="example2Text">Decide from the middle frame of a video.</p><i aria-hidden="true">↗</i></button></div></section>
38
  <section class="code-section"><details id="code-details"><summary><span data-i18n="codeTitle">Make it your own · API example</span><span class="code-tag">PYTHON</span></summary><p data-i18n="codeNote">Code follows your current input. Generating a preview does not call the API.</p><div class="code-actions"><button id="preview-code" type="button" data-i18n="preview">Update preview</button><button id="copy-code" type="button" data-i18n="copy">Copy Python</button><span id="copy-status" role="status"></span></div><pre><code id="python-code"></code></pre><details><summary data-i18n="requestPreview">Request JSON · image data omitted</summary><pre id="request-json"></pre></details></details></section>
39
  <footer><span>© 2026 Valen Team / 万澜</span><p data-i18n="network">Your browser connects directly to api.valen-team.workers.dev. This Space uses the same API as the website.</p><a href="https://huggingface.co/spaces/Valen-Team/Valen-API/tree/main" target="_blank" rel="noreferrer" data-i18n="source">View source ↗</a></footer>
40
  </main>
locales.mjs CHANGED
@@ -1,22 +1,31 @@
1
  export const words = {
2
  en: {
3
- docs:'Documentation ↗', eyebrow:'THE VALEN API PLAYGROUND', title:'A question. A clear decision.', subtitle:'Bring text or an image. Get probabilities for what comes next.', free:'Free API · No key needed', formats:'Yes / No · Choices · Rating', input:'Your input', model:'Model', task:'Output', predicate:'Yes / No', choice:'Choices', score:'Rating', question:'Question', context:'Context · optional', questionPlaceholder:'What would you like Valen to decide?', contextPlaceholder:'Paste the text to assess, or add some context…', choicesLabel:'Choices · one per line', levelsLabel:'Rating levels · low to high, one per line', optionsHint:'2–8 entries, up to 300 characters each.', attach:'Add an image', imageHint:'Optional · JPG, PNG, WebP · up to 3 MB', remove:'Remove', submit:'Ask Valen', cancel:'Cancel request', privacy:'On submit, your input is sent directly to the Valen API.', result:'Decision', emptyTitle:'Small input. Useful signal.', emptyText:'Your result and probability distribution will appear here.', scoreNote:'Rating is the probability-weighted level index, starting at 0. Five levels span 0–4.', raw:'Response JSON', quota:'Free capacity is limited. On HTTP 429, wait at least one minute before retrying.', examplesTitle:'Start with a question', examplesNote:'Loads an example. You choose when to send.', example0Tag:'YES / NO', example0Title:'A happy customer?', example0Text:'Find the sentiment behind a message.', example1Tag:'CHOICES', example1Title:'What do they need?', example1Text:'Choose the right next step for support.', example2Tag:'RATING', example2Title:'Ready to buy?', example2Text:'Rate purchase intent on five levels.', codeTitle:'Make it your own · API example', codeNote:'Code follows your current input. Generating a preview does not call the API.', preview:'Update preview', copy:'Copy Python', copied:'Copied', copyFailed:'Select the code and copy it manually.', requestPreview:'Request JSON · image data omitted', network:'Your browser connects directly to api.valen-team.workers.dev. This Space uses the same API as the website.', source:'View source ↗', ready:'Ready', busy:'Deciding…', success:'Complete', error:'Try again', reading:'Reading image…', cancelled:'Request cancelled.', waiting:'Valen is deciding…', yes:'Yes', no:'No', confidence:'Confidence', modelId:'Returned model ID', noCode:'Enter a valid question and options to generate your API example.',
4
- errors:{model:'Choose a supported model and output type.',question:'Enter a question (up to 1000 characters).',context:'Context can be up to 4000 characters.',options:'Enter 2–8 options, one per line, up to 300 characters each.',image:'Choose a readable JPG, PNG or WebP image.',size:'The image is too large. Choose a file under 3 MB.',response:'The API returned an unexpected response. Please retry later.',network:'Could not connect to the API. Check your network or proxy and try again.',timeout:'The request timed out after two minutes. Please retry later.',http_400:'HTTP 400 · The API rejected this request. Check the question and options.',http_401:'HTTP 401 · API authentication failed.',http_403:'HTTP 403 · This website is not allowed to call the API.',http_413:'HTTP 413 · The request is too large.',http_429:'HTTP 429 · Free API capacity is limited. Wait at least one minute before retrying.',http_502:'HTTP 502 · The model service could not complete the request.',http_503:'HTTP 503 · The API is not configured or is unavailable.'}
5
  },
6
  zh: {
7
- docs:'开发者文档 ↗',eyebrow:'VALEN API 体验',title:'一个问题,一次清晰判断。',subtitle:'输入文字或图片,让下一步决策有据可依。',free:'免费 API · 无需 Key',formats:'是 / 否 · 选择 · 评分',input:'你的输入',model:'模型',task:'输出类型',predicate:'是 / 否',choice:'选择答案',score:'评分',question:'问题',context:'补充背景 · 可选',questionPlaceholder:'你想让 Valen 判断什么?',contextPlaceholder:'粘贴需要判断的文字,或补充相关信息…',choicesLabel:'候选项 · 每行一个',levelsLabel:'评分档位 · 从低到高,每行一个',optionsHint:'2–8 项,每项最多 300 字符。',attach:'添加图片',imageHint:'可选 · JPG、PNG、WebP · 最大 3 MB',remove:'移除',submit:'提交判断',cancel:'取消请求',privacy:'提交后,你的输入会直接发送至 Valen API。',result:'判断结果',emptyTitle:'简单输入,清晰信号。',emptyText:'判断结果与各个选项的概率将在这里显示。',scoreNote:'评分按从 0 开始的档位索引进行概率加权。五个档位对应 0–4 分。',raw:'原始响应 JSON',quota:'免费 API 容量有限。遇到 HTTP 429,请至少等待一分钟后重试。',examplesTitle:'从一个问题开始',examplesNote:'点击填入示例,提交时才会请求模型。',example0Tag:'是非判断',example0Title:'客户满意吗?',example0Text:'从一段反馈中判断客户的态度。',example1Tag:'选择答案',example1Title:'客户需要什么?',example1Text:'为客服选择合适的下一步处理方式。',example2Tag:'评分',example2Title:'准备好购买了吗?',example2Text:'通过五个档位判断客户的购买意愿。',codeTitle:'接入你的应用 · API 示例',codeNote:'代码根据当前输入生成。生成预览不会调用 API。',preview:'更新预览',copy:'复制 Python',copied:'已复制',copyFailed:'请选中代码后手动复制。',requestPreview:'请求 JSON · 已省略图片数据',network:'浏览器直接连接 api.valen-team.workers.dev。此 Space 与官网共用同一个 API。',source:'查看源码 ↗',ready:'等待提问',busy:'正在判断…',success:'已完成',error:'请求未完成',reading:'正在读取图片…',cancelled:'请求已取消。',waiting:'Valen 正在判断…',yes:'是',no:'否',confidence:'置信度',modelId:'返回的模型标识',noCode:'填写有效的问题与选项后,即可生成对应的 API 示例。',
8
- errors:{model:'请选择支持的模型与输出类型。',question:'请填写问题,最多 1000 字符。',context:'补充背景最多 4000 字符。',options:'请输入 2–8 项,每行一个,每项最多 300 字符。',image:'请选择可读取的 JPG、PNG 或 WebP 图片。',size:'图片过大,请选择小于 3 MB 的文件。',response:'API 返回的格式异常,请稍后重试。',network:'无法连接 API,请检查当前网络或代理后重试。',timeout:'请求等待超过两分钟,请稍后重试。',http_400:'HTTP 400 · API 拒绝了请求,请检查问题与选项。',http_401:'HTTP 401 · API 鉴权失败。',http_403:'HTTP 403 · 当前网站尚未获准调用 API。',http_413:'HTTP 413 · 请求体积超出限制。',http_429:'HTTP 429 · 免费 API 额度受限,请至少等待一分钟后重试。',http_502:'HTTP 502 · 模型服务暂时无法完成请求。',http_503:'HTTP 503 · API 尚未配置或暂不可用。'}
9
  }
10
  };
11
  export const examples = {
12
  en: [
13
- ['predicate','Does this customer sound satisfied?','The delivery arrived early and everything works perfectly. Thank you!',''],
14
- ['choice','What does this customer want?','I ordered a blue jacket but received a red one. Can you send the right color?','Request a refund\nExchange an item\nTrack delivery'],
15
- ['score','Rate the purchase intent.','This looks like a great fit. Can you send a quote for 20 seats? We plan to start next week.','No interest\nSlight interest\nConsidering\nStrong interest\nReady to buy']
16
  ],
17
  zh: [
18
- ['predicate','这位客户对服务满意吗?','快递比预计时间还早到,所有东西都很好用。谢谢!',''],
19
- ['choice','这位客户想做什么?','我订的是蓝色外套,收到的却是红色。可以帮我换成正确的颜色吗?','申请退款\n更换商品\n查询物流'],
20
- ['score','请评估这位客户的购买意愿。','这个方案很适合我们。可以发一份 20 个席位的报价吗?我们计划下周就开始使用。','没有兴趣\n略有兴趣\n正在考虑\n意愿较强\n准备购买']
21
  ]
22
  };
 
 
 
 
 
 
 
 
 
 
1
  export const words = {
2
  en: {
3
+ docs:'Documentation ↗', eyebrow:'THE VALEN API PLAYGROUND', title:'A question. A clear decision.', subtitle:'Bring text, an image, or a video. Get probabilities for what comes next.', free:'Free API · No key needed', formats:'Yes / No · Choices · Rating', input:'Your input', model:'Model', task:'Output', predicate:'Yes / No', choice:'Choices', score:'Rating', question:'Question', context:'Context · optional', questionPlaceholder:'What would you like Valen to decide?', contextPlaceholder:'Paste the text to assess, or add some context…', choicesLabel:'Choices · one per line', levelsLabel:'Rating levels · low to high, one per line', optionsHint:'2–8 entries, up to 300 characters each.', attach:'Add an image or video', imageHint:'Images up to 3 MB · MP4 / WebM up to 20 MB', remove:'Remove', submit:'Ask Valen', cancel:'Cancel request', privacy:'On submit, your input is sent directly to the Valen API.', result:'Decision', emptyTitle:'Small input. Useful signal.', emptyText:'Your result and probability distribution will appear here.', scoreNote:'Rating is the probability-weighted level index, starting at 0. Five levels span 0–4.', raw:'Response JSON', quota:'Free capacity is limited. On HTTP 429, wait at least one minute before retrying.', examplesTitle:'Start with a question', examplesNote:'Loads an example. You choose when to send.', example0Tag:'TEXT', example0Title:'What does this customer need?', example0Text:'Read a message and identify its intent.', example1Tag:'IMAGE', example1Title:'What is this group called?', example1Text:'Look at the animals and choose a name.', example2Tag:'VIDEO', example2Title:'What color is the sauce?', example2Text:'Decide from the middle frame of a video.', codeTitle:'Make it your own · API example', codeNote:'Code follows your current input. Generating a preview does not call the API.', preview:'Update preview', copy:'Copy Python', copied:'Copied', copyFailed:'Select the code and copy it manually.', requestPreview:'Request JSON · image data omitted', network:'Your browser connects directly to api.valen-team.workers.dev. This Space uses the same API as the website.', source:'View source ↗', ready:'Ready', busy:'Deciding…', success:'Complete', error:'Try again', reading:'Reading attachment…',videoNote:'Only a JPEG of the middle frame is sent. A single frame cannot establish motion or event order.', cancelled:'Request cancelled.', waiting:'Valen is deciding…', yes:'Yes', no:'No', confidence:'Confidence', modelId:'Returned model ID', noCode:'Enter a valid question and options to generate your API example.',
4
+ errors:{model:'Choose a supported model and output type.',question:'Enter a question (up to 1000 characters).',context:'Context can be up to 4000 characters.',options:'Enter 2–8 options, one per line, up to 300 characters each.',image:'Could not read the attachment. Choose JPG, PNG, WebP, or a browser-playable MP4 / WebM.',size:'Images can be up to 3 MB; videos up to 20 MB.',response:'The API returned an unexpected response. Please retry later.',network:'Could not connect to the API. Check your network or proxy and try again.',timeout:'The request timed out after two minutes. Please retry later.',http_400:'HTTP 400 · The API rejected this request. Check the question and options.',http_401:'HTTP 401 · API authentication failed.',http_403:'HTTP 403 · This website is not allowed to call the API.',http_413:'HTTP 413 · The request is too large.',http_429:'HTTP 429 · Free API capacity is limited. Wait at least one minute before retrying.',http_502:'HTTP 502 · The model service could not complete the request.',http_503:'HTTP 503 · The API is not configured or is unavailable.'}
5
  },
6
  zh: {
7
+ docs:'开发者文档 ↗',eyebrow:'VALEN API 体验',title:'一个问题,一次清晰判断。',subtitle:'输入文字、图片或视频,让下一步决策有据可依。',free:'免费 API · 无需 Key',formats:'是 / 否 · 选择 · 评分',input:'你的输入',model:'模型',task:'输出类型',predicate:'是 / 否',choice:'选择答案',score:'评分',question:'问题',context:'补充背景 · 可选',questionPlaceholder:'你想让 Valen 判断什么?',contextPlaceholder:'粘贴需要判断的文字,或补充相关信息…',choicesLabel:'候选项 · 每行一个',levelsLabel:'评分档位 · 从低到高,每行一个',optionsHint:'2–8 项,每项最多 300 字符。',attach:'添加图片或视频',imageHint:'图片最大 3 MB · MP4 / WebM 视频最大 20 MB',remove:'移除',submit:'提交判断',cancel:'取消请求',privacy:'提交后,你的输入会直接发送至 Valen API。',result:'判断结果',emptyTitle:'简单输入,清晰信号。',emptyText:'判断结果与各个选项的概率将在这里显示。',scoreNote:'评分按从 0 开始的档位索引进行概率加权。五个档位对应 0–4 分。',raw:'原始响应 JSON',quota:'免费 API 容量有限。遇到 HTTP 429,请至少等待一分钟后重试。',examplesTitle:'从一个问题开始',examplesNote:'点击填入示例,提交时才会请求模型。',example0Tag:'纯文本',example0Title:'这位客户想做什么?',example0Text:'读一段客户消息,判断它表达的意图。',example1Tag:'图片',example1Title:'这群动物应该怎样称呼?',example1Text:'看一张动物图片,选择正确的群体名称。',example2Tag:'视频',example2Title:'左边的酱料是什么颜色?',example2Text:'提取视频中间帧,根据可见画面判断。',codeTitle:'接入你的应用 · API 示例',codeNote:'代码根据当前输入生成。生成预览不会调用 API。',preview:'更新预览',copy:'复制 Python',copied:'已复制',copyFailed:'请选中代码后手动复制。',requestPreview:'请求 JSON · 已省略图片数据',network:'浏览器直接连接 api.valen-team.workers.dev。此 Space 与官网共用同一个 API。',source:'查看源码 ↗',ready:'等待提问',busy:'正在判断…',success:'已完成',error:'请求未完成',reading:'正在读取附件…',videoNote:'只发送视频中间帧的 JPEG 图片。单帧无法判断运动或事件顺序。',cancelled:'请求已取消。',waiting:'Valen 正在判断…',yes:'是',no:'否',confidence:'置信度',modelId:'返回的模型标识',noCode:'填写有效的问题与选项后,即可生成对应的 API 示例。',
8
+ errors:{model:'请选择支持的模型与输出类型。',question:'请填写问题,最多 1000 字符。',context:'补充背景最多 4000 字符。',options:'请输入 2–8 项,每行一个,每项最多 300 字符。',image:'无法读取附件。请选择 JPG、PNG、WebP,或浏览器可播放的 MP4 / WebM。',size:'图片最大 3 MB,视频最大 20 MB。',response:'API 返回的格式异常,请稍后重试。',network:'无法连接 API,请检查当前网络或代理后重试。',timeout:'请求等待超过两分钟,请稍后重试。',http_400:'HTTP 400 · API 拒绝了请求,请检查问题与选项。',http_401:'HTTP 401 · API 鉴权失败。',http_403:'HTTP 403 · 当前网站尚未获准调用 API。',http_413:'HTTP 413 · 请求体积超出限制。',http_429:'HTTP 429 · 免费 API 额度受限,请至少等待一分钟后重试。',http_502:'HTTP 502 · 模型服务暂时无法完成请求。',http_503:'HTTP 503 · API 尚未配置或暂不可用。'}
9
  }
10
  };
11
  export const examples = {
12
  en: [
13
+ ['choice', "Which intent does the user's message express?", 'I moved. Can you ship my order to 12 Elm Street instead of the old address?', 'Asks about a charge, invoice or payment\nWants to cancel an order\nWants to change the delivery address\nReceived an item that is broken or damaged\nWants to know where an order is or when it arrives'],
14
+ ['choice', 'What is a group of these animals called?', 'Answer using only the visible image.', 'pride\nflock\nclowder\ngang'],
15
+ ['choice', 'What is the color of the sauce in the foil container on the left?', 'Answer using only the visible image.', 'Brown\nWhite\nRed\nOrange']
16
  ],
17
  zh: [
18
+ ['choice', '这条客户消息表达了什么意图?', '我搬家了,可以把订单寄到榆树街 12 号,而不是原来的地址吗?', '咨询费用、发票或付款\n取消订单\n更改收货地址\n反馈收到的商品破损\n查询订单位置或预计送达时间'],
19
+ ['choice', '图片中的这群动物应该怎样称呼?', '请仅根据图片中可见的内容回答问题。', '狮群\n羊群\n猫群\n猴群'],
20
+ ['choice', '左边铝箔容器里的酱料是什么颜色?', '请仅根据视频画面中可见的内容回答问题。', '棕色\n白色\n红色\n橙色']
21
  ]
22
  };
23
+ export const exampleMedia = [
24
+ null,
25
+ {kind:'image',src:'./cookbook/sheep.jpg',name:'sheep.jpg'},
26
+ {kind:'video',src:'./cookbook/momos.mp4',name:'momos.mp4'}
27
+ ];
28
+ export const ratingOptions = {
29
+ en:'Very low\nLow\nModerate\nHigh\nVery high',
30
+ zh:'很低\n较低\n中等\n较高\n很高'
31
+ };
media.mjs ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Browser-side preprocessing adapted from the Valen website's media.ts.
2
+ function once(target, event, signal) {
3
+ return new Promise((resolve, reject) => {
4
+ const finish = error => {
5
+ clearTimeout(timer);target.removeEventListener(event, success);target.removeEventListener('error', failure);
6
+ signal.removeEventListener('abort', abort);error ? reject(error) : resolve();
7
+ };
8
+ const success = () => finish();
9
+ const failure = () => finish(new Error('image'));
10
+ const abort = () => finish(new DOMException('Aborted','AbortError'));
11
+ const timer = setTimeout(failure,20000);
12
+ target.addEventListener(event,success,{once:true});target.addEventListener('error',failure,{once:true});
13
+ signal.addEventListener('abort',abort,{once:true});if(signal.aborted)abort();
14
+ });
15
+ }
16
+ function jpeg(source, width, height) {
17
+ if(!width||!height)throw new Error('image');
18
+ const scale=Math.min(1,1536/Math.max(width,height));
19
+ const canvas=document.createElement('canvas');canvas.width=Math.max(1,Math.round(width*scale));canvas.height=Math.max(1,Math.round(height*scale));
20
+ const ctx=canvas.getContext('2d');if(!ctx)throw new Error('image');
21
+ ctx.fillStyle='#fff';ctx.fillRect(0,0,canvas.width,canvas.height);ctx.drawImage(source,0,0,canvas.width,canvas.height);
22
+ return canvas.toDataURL('image/jpeg',.85);
23
+ }
24
+ export async function readMedia(file, signal) {
25
+ const videoType=['video/mp4','video/webm'].includes(file.type);
26
+ if(!videoType&&!['image/jpeg','image/png','image/webp'].includes(file.type))throw new Error('image');
27
+ if(file.size>(videoType?20:3)*1000000)throw new Error('size');
28
+ const url=URL.createObjectURL(file);
29
+ try {
30
+ if(!videoType) {
31
+ const image=new Image();const loaded=once(image,'load',signal);image.src=url;await loaded;
32
+ return {kind:'image',name:file.name,image:jpeg(image,image.naturalWidth,image.naturalHeight),objectUrl:url};
33
+ }
34
+ const video=document.createElement('video');video.muted=true;video.playsInline=true;video.preload='auto';
35
+ try {
36
+ const loaded=once(video,'loadeddata',signal);video.src=url;await loaded;
37
+ if(!Number.isFinite(video.duration)||video.duration<=0)throw new Error('image');
38
+ if(video.duration/2>.01){const seeked=once(video,'seeked',signal);video.currentTime=video.duration/2;await seeked;}
39
+ return {kind:'video',name:file.name,image:jpeg(video,video.videoWidth,video.videoHeight),video:url,objectUrl:url};
40
+ } finally {video.removeAttribute('src');video.load();}
41
+ } catch(error) {URL.revokeObjectURL(url);throw error;}
42
+ }
style.css CHANGED
@@ -1,3 +1,5 @@
1
  @import url('./assets/modern-tokens.css');
2
  @font-face{font-family:'DM Sans';src:url('./assets/dmsans.woff2') format('woff2');font-display:swap}
3
  *{box-sizing:border-box}body{margin:0;background:var(--bg-doc);color:var(--text);font:15px/1.5 var(--font-body)}body[data-theme=dark]{--bg-doc:#101319;--bg:#101319;--surface:#1b202a;--text:#e4e9f3;--heading:#f0f4ff;--text-muted:#a1adbf;--text-faint:#8794a7;--border:#30394a;--primary:#8aa5ff;--primary-soft:#202e53;--green-bg:#173828;--green:#83d5a0;--red:#ff9b9b;--red-bg:#3b2027}button,input,select,textarea{font:inherit}button,a,input,select,textarea,summary{-webkit-tap-highlight-color:transparent}button{cursor:pointer}button:disabled{cursor:wait;opacity:.55}a{color:inherit;text-decoration:none}a:hover{color:var(--primary)}button,select,textarea{outline-offset:4px}button:focus-visible,a:focus-visible,summary:focus-visible{outline:2px solid var(--primary);outline-offset:4px}[hidden]{display:none!important}.nav{height:72px;display:flex;justify-content:space-between;align-items:center;max-width:1200px;margin:auto;padding:0 28px;border-bottom:1px solid var(--border)}.brand{display:flex;align-items:center;gap:9px}.brand img{width:36px;height:36px;object-fit:cover;border-radius:8px}.brand strong{font-size:27px;letter-spacing:-1.3px;color:var(--heading)}.brand span{font:11px var(--font-mono);color:var(--text-muted);border-left:1px solid var(--border);padding-left:14px;margin-left:6px}.nav nav{display:flex;align-items:center;gap:22px;font-size:13px}.nav button{background:none;color:var(--text);border:0;padding:6px}.nav #theme{font-size:22px}main{max-width:1200px;padding:0 28px;margin:auto}.intro{padding:55px 0 34px}.eyebrow{font-size:11px;letter-spacing:2px;color:var(--primary);font-weight:700}h1{font-size:clamp(30px,4.5vw,46px);line-height:1.15;color:var(--heading);letter-spacing:-1.5px;margin:15px 0}h2,h3,p{margin-top:0}.intro p{color:var(--text-muted);font-size:17px;margin-bottom:20px}.intro-meta{display:flex;align-items:center;gap:8px;font-size:12px;color:var(--text-muted)}.dot{height:6px;width:6px;background:var(--green);border-radius:50%}.divider{padding:0 8px;color:var(--text-faint)}.workspace{display:grid;grid-template-columns:1fr 1fr;gap:24px}.panel{border:1px solid var(--border);border-radius:16px;padding:25px;min-width:0}.panel-heading{display:flex;align-items:center;gap:10px;margin-bottom:22px}.panel-heading h2{font-size:16px;margin:0;letter-spacing:-.2px}.step{font:11px var(--font-mono);color:var(--text-faint)}fieldset{border:0;padding:0;margin:0;min-width:0}label{display:block;margin-bottom:17px}label>span{display:block;font-size:12px;font-weight:600;margin-bottom:7px}label small{color:var(--text-muted);font-size:11px;font-weight:400;margin-left:5px}.two-columns{display:grid;grid-template-columns:1fr 1fr;gap:15px}select,textarea{width:100%;border:1px solid var(--border);background:var(--bg-doc);border-radius:8px;color:var(--text);padding:10px 12px;font-size:13px}textarea{resize:vertical;min-height:75px;line-height:1.6;display:block}textarea::placeholder{color:var(--text-faint)}select:focus,textarea:focus{outline:2px solid var(--primary-soft);border-color:var(--primary)}.attachment{border:1px dashed var(--border-strong,var(--border));border-radius:10px;margin-bottom:18px;padding:12px}.upload-label{display:flex;gap:10px;align-items:center;margin:0;cursor:pointer}.upload-label span{margin:0}.upload-label strong{font-size:12px}.upload-label small{display:block;margin:2px 0 0}.upload-icon{font-size:26px;color:var(--primary)}#image{width:100%;font-size:11px;margin-top:10px;color:var(--text-muted)}#image::file-selector-button{border:1px solid var(--border);padding:4px 8px;border-radius:5px;background:var(--surface);color:var(--text);margin-right:8px}#image-preview{display:flex;align-items:center;gap:12px;padding-top:10px}#thumbnail{width:48px;height:48px;object-fit:cover;border-radius:5px}#image-name{min-width:0;overflow-wrap:anywhere;font-size:12px}#remove-image{margin-left:auto;border:0;background:none;color:var(--text-muted);font-size:12px}.primary{width:100%;display:flex;justify-content:center;gap:10px;background:var(--primary);color:white;border:0;border-radius:8px;padding:12px;font-weight:600;font-size:14px}.primary:hover{filter:brightness(.95)}body[data-theme=dark] .primary{color:#101319}.privacy{font-size:11px;text-align:center;color:var(--text-muted);margin:12px 0 0}.cancel{background:var(--surface);border:1px solid var(--border);color:var(--text);padding:6px 12px;border-radius:6px;display:block;margin:10px auto 0;font-size:12px}.output-panel{display:flex;flex-direction:column}.badge{margin-left:auto;color:var(--text-muted);background:var(--surface);padding:4px 9px;border-radius:100px;font-size:10px}.badge[data-state=success]{color:var(--green);background:var(--green-bg)}.badge[data-state=error]{color:var(--red);background:var(--red-bg)}.badge[data-state=busy]{color:var(--primary);background:var(--primary-soft)}#status{font-size:13px;color:var(--text-muted);overflow-wrap:anywhere}#status.error{color:var(--red);padding:12px;background:var(--red-bg);border-radius:8px;margin-bottom:18px}#empty{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;min-height:290px;padding:25px 10px}.empty-mark{color:var(--primary);font-size:26px;background:var(--primary-soft);width:58px;height:58px;display:grid;place-items:center;border-radius:16px;margin-bottom:22px;font-weight:600}#empty h3{font-size:19px;letter-spacing:-.4px;margin-bottom:8px}#empty p{max-width:270px;color:var(--text-muted);font-size:13px}#answer{padding:8px 0 24px}#decision-title{font-size:26px;line-height:1.3;overflow-wrap:anywhere;color:var(--heading);margin:12px 0 26px}.prob-row{margin:0 0 16px}.prob-label{display:flex;justify-content:space-between;gap:15px;margin-bottom:6px;font-size:13px}.prob-label span{overflow-wrap:anywhere}.prob-label strong{font:12px var(--font-mono);white-space:nowrap}progress{display:block;width:100%;height:6px;appearance:none;border:0;border-radius:6px;overflow:hidden;background:var(--surface)}progress::-webkit-progress-bar{background:var(--surface)}progress::-webkit-progress-value{background:var(--primary);border-radius:6px}progress::-moz-progress-bar{background:var(--primary);border-radius:6px}.note{font-size:12px;color:var(--text-muted);margin:20px 0 10px}.model-id{font:10px/1.5 var(--font-mono);color:var(--text-faint);overflow-wrap:anywhere}.api-note{border-top:1px solid var(--border);padding-top:18px;margin-top:auto}.mini-label{color:var(--green);background:var(--green-bg);font:10px var(--font-mono);padding:3px 5px;border-radius:4px;margin-right:7px}.api-note code{font:11px var(--font-mono)}.api-note p{font-size:11px;color:var(--text-muted);margin:10px 0 0}.examples{margin-top:35px}.section-heading{display:flex;justify-content:space-between;align-items:center;gap:15px;margin-bottom:15px}.section-heading h2{font-size:16px;margin:0}.section-heading>span{font-size:11px;color:var(--text-muted)}.example-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:16px}.example-grid button{position:relative;background:var(--bg-doc);border:1px solid var(--border);border-radius:12px;padding:19px;text-align:left;color:var(--text)}.example-grid button:hover{border-color:var(--primary);background:var(--primary-soft)}.example-grid button>span{display:block;font-size:9px;letter-spacing:1.1px;color:var(--text-muted);margin-bottom:13px}.example-grid strong{font-size:15px;font-weight:600}.example-grid p{font-size:12px;color:var(--text-muted);margin:6px 0 0}.example-grid i{position:absolute;right:17px;top:15px;color:var(--text-muted);font-style:normal}.code-section{margin:32px 0;border:1px solid var(--border);padding:18px 22px;border-radius:12px}summary{cursor:pointer;font-size:12px;font-weight:600;line-height:1.6}summary::marker{color:var(--text-muted)}.code-section>details>summary{font-size:14px}.code-tag{float:right;font:9px var(--font-mono);color:var(--text-muted);padding-top:4px}.code-section p{font-size:12px;color:var(--text-muted);margin:16px 0}.code-actions{display:flex;gap:8px;align-items:center}.code-actions button{background:var(--surface);color:var(--text);border:1px solid var(--border);border-radius:6px;padding:6px 10px;font-size:11px}#copy-status{font-size:11px;color:var(--text-muted)}pre{background:var(--surface);border-radius:8px;padding:16px;font:11px/1.65 var(--font-mono);overflow:auto;max-height:420px;white-space:pre-wrap;overflow-wrap:anywhere}.code-section>details>pre{background:var(--code-bg);color:#e2e6ef;margin-top:15px}footer{border-top:1px solid var(--border);padding:20px 0 30px;display:flex;align-items:flex-start;gap:30px;font-size:10px;color:var(--text-muted)}footer p{margin:0;max-width:550px}footer>a{margin-left:auto;white-space:nowrap}footer>span{white-space:nowrap}@media(max-width:760px){.nav{padding:0 18px;height:62px}.nav nav{gap:12px}.nav nav>a{font-size:11px}main{padding:0 18px}.intro{padding:35px 0 26px}.intro p{font-size:14px}.intro-meta{font-size:10px;flex-wrap:wrap}.workspace{grid-template-columns:1fr;gap:16px}.panel{padding:19px}.example-grid{grid-template-columns:1fr}.example-grid button{padding:15px}.example-grid button>span{margin-bottom:6px}.section-heading{align-items:flex-start;flex-direction:column;gap:5px}#empty{min-height:200px}footer{flex-direction:column;gap:12px}footer>a{margin-left:0}.code-section{padding:15px}.code-tag{display:none}}
 
 
 
1
  @import url('./assets/modern-tokens.css');
2
  @font-face{font-family:'DM Sans';src:url('./assets/dmsans.woff2') format('woff2');font-display:swap}
3
  *{box-sizing:border-box}body{margin:0;background:var(--bg-doc);color:var(--text);font:15px/1.5 var(--font-body)}body[data-theme=dark]{--bg-doc:#101319;--bg:#101319;--surface:#1b202a;--text:#e4e9f3;--heading:#f0f4ff;--text-muted:#a1adbf;--text-faint:#8794a7;--border:#30394a;--primary:#8aa5ff;--primary-soft:#202e53;--green-bg:#173828;--green:#83d5a0;--red:#ff9b9b;--red-bg:#3b2027}button,input,select,textarea{font:inherit}button,a,input,select,textarea,summary{-webkit-tap-highlight-color:transparent}button{cursor:pointer}button:disabled{cursor:wait;opacity:.55}a{color:inherit;text-decoration:none}a:hover{color:var(--primary)}button,select,textarea{outline-offset:4px}button:focus-visible,a:focus-visible,summary:focus-visible{outline:2px solid var(--primary);outline-offset:4px}[hidden]{display:none!important}.nav{height:72px;display:flex;justify-content:space-between;align-items:center;max-width:1200px;margin:auto;padding:0 28px;border-bottom:1px solid var(--border)}.brand{display:flex;align-items:center;gap:9px}.brand img{width:36px;height:36px;object-fit:cover;border-radius:8px}.brand strong{font-size:27px;letter-spacing:-1.3px;color:var(--heading)}.brand span{font:11px var(--font-mono);color:var(--text-muted);border-left:1px solid var(--border);padding-left:14px;margin-left:6px}.nav nav{display:flex;align-items:center;gap:22px;font-size:13px}.nav button{background:none;color:var(--text);border:0;padding:6px}.nav #theme{font-size:22px}main{max-width:1200px;padding:0 28px;margin:auto}.intro{padding:55px 0 34px}.eyebrow{font-size:11px;letter-spacing:2px;color:var(--primary);font-weight:700}h1{font-size:clamp(30px,4.5vw,46px);line-height:1.15;color:var(--heading);letter-spacing:-1.5px;margin:15px 0}h2,h3,p{margin-top:0}.intro p{color:var(--text-muted);font-size:17px;margin-bottom:20px}.intro-meta{display:flex;align-items:center;gap:8px;font-size:12px;color:var(--text-muted)}.dot{height:6px;width:6px;background:var(--green);border-radius:50%}.divider{padding:0 8px;color:var(--text-faint)}.workspace{display:grid;grid-template-columns:1fr 1fr;gap:24px}.panel{border:1px solid var(--border);border-radius:16px;padding:25px;min-width:0}.panel-heading{display:flex;align-items:center;gap:10px;margin-bottom:22px}.panel-heading h2{font-size:16px;margin:0;letter-spacing:-.2px}.step{font:11px var(--font-mono);color:var(--text-faint)}fieldset{border:0;padding:0;margin:0;min-width:0}label{display:block;margin-bottom:17px}label>span{display:block;font-size:12px;font-weight:600;margin-bottom:7px}label small{color:var(--text-muted);font-size:11px;font-weight:400;margin-left:5px}.two-columns{display:grid;grid-template-columns:1fr 1fr;gap:15px}select,textarea{width:100%;border:1px solid var(--border);background:var(--bg-doc);border-radius:8px;color:var(--text);padding:10px 12px;font-size:13px}textarea{resize:vertical;min-height:75px;line-height:1.6;display:block}textarea::placeholder{color:var(--text-faint)}select:focus,textarea:focus{outline:2px solid var(--primary-soft);border-color:var(--primary)}.attachment{border:1px dashed var(--border-strong,var(--border));border-radius:10px;margin-bottom:18px;padding:12px}.upload-label{display:flex;gap:10px;align-items:center;margin:0;cursor:pointer}.upload-label span{margin:0}.upload-label strong{font-size:12px}.upload-label small{display:block;margin:2px 0 0}.upload-icon{font-size:26px;color:var(--primary)}#image{width:100%;font-size:11px;margin-top:10px;color:var(--text-muted)}#image::file-selector-button{border:1px solid var(--border);padding:4px 8px;border-radius:5px;background:var(--surface);color:var(--text);margin-right:8px}#image-preview{display:flex;align-items:center;gap:12px;padding-top:10px}#thumbnail{width:48px;height:48px;object-fit:cover;border-radius:5px}#image-name{min-width:0;overflow-wrap:anywhere;font-size:12px}#remove-image{margin-left:auto;border:0;background:none;color:var(--text-muted);font-size:12px}.primary{width:100%;display:flex;justify-content:center;gap:10px;background:var(--primary);color:white;border:0;border-radius:8px;padding:12px;font-weight:600;font-size:14px}.primary:hover{filter:brightness(.95)}body[data-theme=dark] .primary{color:#101319}.privacy{font-size:11px;text-align:center;color:var(--text-muted);margin:12px 0 0}.cancel{background:var(--surface);border:1px solid var(--border);color:var(--text);padding:6px 12px;border-radius:6px;display:block;margin:10px auto 0;font-size:12px}.output-panel{display:flex;flex-direction:column}.badge{margin-left:auto;color:var(--text-muted);background:var(--surface);padding:4px 9px;border-radius:100px;font-size:10px}.badge[data-state=success]{color:var(--green);background:var(--green-bg)}.badge[data-state=error]{color:var(--red);background:var(--red-bg)}.badge[data-state=busy]{color:var(--primary);background:var(--primary-soft)}#status{font-size:13px;color:var(--text-muted);overflow-wrap:anywhere}#status.error{color:var(--red);padding:12px;background:var(--red-bg);border-radius:8px;margin-bottom:18px}#empty{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;min-height:290px;padding:25px 10px}.empty-mark{color:var(--primary);font-size:26px;background:var(--primary-soft);width:58px;height:58px;display:grid;place-items:center;border-radius:16px;margin-bottom:22px;font-weight:600}#empty h3{font-size:19px;letter-spacing:-.4px;margin-bottom:8px}#empty p{max-width:270px;color:var(--text-muted);font-size:13px}#answer{padding:8px 0 24px}#decision-title{font-size:26px;line-height:1.3;overflow-wrap:anywhere;color:var(--heading);margin:12px 0 26px}.prob-row{margin:0 0 16px}.prob-label{display:flex;justify-content:space-between;gap:15px;margin-bottom:6px;font-size:13px}.prob-label span{overflow-wrap:anywhere}.prob-label strong{font:12px var(--font-mono);white-space:nowrap}progress{display:block;width:100%;height:6px;appearance:none;border:0;border-radius:6px;overflow:hidden;background:var(--surface)}progress::-webkit-progress-bar{background:var(--surface)}progress::-webkit-progress-value{background:var(--primary);border-radius:6px}progress::-moz-progress-bar{background:var(--primary);border-radius:6px}.note{font-size:12px;color:var(--text-muted);margin:20px 0 10px}.model-id{font:10px/1.5 var(--font-mono);color:var(--text-faint);overflow-wrap:anywhere}.api-note{border-top:1px solid var(--border);padding-top:18px;margin-top:auto}.mini-label{color:var(--green);background:var(--green-bg);font:10px var(--font-mono);padding:3px 5px;border-radius:4px;margin-right:7px}.api-note code{font:11px var(--font-mono)}.api-note p{font-size:11px;color:var(--text-muted);margin:10px 0 0}.examples{margin-top:35px}.section-heading{display:flex;justify-content:space-between;align-items:center;gap:15px;margin-bottom:15px}.section-heading h2{font-size:16px;margin:0}.section-heading>span{font-size:11px;color:var(--text-muted)}.example-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:16px}.example-grid button{position:relative;background:var(--bg-doc);border:1px solid var(--border);border-radius:12px;padding:19px;text-align:left;color:var(--text)}.example-grid button:hover{border-color:var(--primary);background:var(--primary-soft)}.example-grid button>span{display:block;font-size:9px;letter-spacing:1.1px;color:var(--text-muted);margin-bottom:13px}.example-grid strong{font-size:15px;font-weight:600}.example-grid p{font-size:12px;color:var(--text-muted);margin:6px 0 0}.example-grid i{position:absolute;right:17px;top:15px;color:var(--text-muted);font-style:normal}.code-section{margin:32px 0;border:1px solid var(--border);padding:18px 22px;border-radius:12px}summary{cursor:pointer;font-size:12px;font-weight:600;line-height:1.6}summary::marker{color:var(--text-muted)}.code-section>details>summary{font-size:14px}.code-tag{float:right;font:9px var(--font-mono);color:var(--text-muted);padding-top:4px}.code-section p{font-size:12px;color:var(--text-muted);margin:16px 0}.code-actions{display:flex;gap:8px;align-items:center}.code-actions button{background:var(--surface);color:var(--text);border:1px solid var(--border);border-radius:6px;padding:6px 10px;font-size:11px}#copy-status{font-size:11px;color:var(--text-muted)}pre{background:var(--surface);border-radius:8px;padding:16px;font:11px/1.65 var(--font-mono);overflow:auto;max-height:420px;white-space:pre-wrap;overflow-wrap:anywhere}.code-section>details>pre{background:var(--code-bg);color:#e2e6ef;margin-top:15px}footer{border-top:1px solid var(--border);padding:20px 0 30px;display:flex;align-items:flex-start;gap:30px;font-size:10px;color:var(--text-muted)}footer p{margin:0;max-width:550px}footer>a{margin-left:auto;white-space:nowrap}footer>span{white-space:nowrap}@media(max-width:760px){.nav{padding:0 18px;height:62px}.nav nav{gap:12px}.nav nav>a{font-size:11px}main{padding:0 18px}.intro{padding:35px 0 26px}.intro p{font-size:14px}.intro-meta{font-size:10px;flex-wrap:wrap}.workspace{grid-template-columns:1fr;gap:16px}.panel{padding:19px}.example-grid{grid-template-columns:1fr}.example-grid button{padding:15px}.example-grid button>span{margin-bottom:6px}.section-heading{align-items:flex-start;flex-direction:column;gap:5px}#empty{min-height:200px}footer{flex-direction:column;gap:12px}footer>a{margin-left:0}.code-section{padding:15px}.code-tag{display:none}}
4
+
5
+ #video-preview{display:block;width:100%;max-height:240px;border-radius:8px;margin-top:12px;background:#000}.example-image{display:block;width:100%;height:110px;object-fit:cover;border-radius:7px;margin:0 0 12px}.example-grid button{align-self:stretch}.example-grid strong{display:block}#video-note{margin:10px 0 0;font-size:11px}