Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Change progress to be between 0 and 1 #30

Merged
merged 1 commit into from
Jan 24, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -213,27 +213,31 @@ if (supportsOurUseCase !== "no") {

### Download progress

In cases where using the API is only possible after a download, you can monitor the download progress (e.g. in order to show your users a progress bar) using code such as the following:
For cases where using the API is only possible after a download, you can monitor the download progress (e.g. in order to show your users a progress bar) using code such as the following:

```js
const writer = await ai.writer.create({
...otherOptions,
monitor(m) {
m.addEventListener("downloadprogress", e => {
console.log(`Downloaded ${e.loaded} of ${e.total} bytes.`);
console.log(`Downloaded ${e.loaded * 100}%`);
});
}
);
```

If the download fails, then `downloadprogress` events will stop being emitted, and the promise returned by `create()` will be rejected with a `"NetworkError"` `DOMException`.
If the download fails, then `downloadprogress` events will stop being fired, and the promise returned by `create()` will be rejected with a `"NetworkError"` `DOMException`.

Note that in the case that multiple entities are downloaded (e.g., a base model plus a [LoRA fine-tuning](https://arxiv.org/abs/2106.09685) for writing, or for the particular style requested) web developers do not get the ability to monitor the individual downloads. All of them are bundled into the overall `downloadprogress` events, and the `create()` promise is not fulfilled until all downloads and loads are successful.

The event is a [`ProgressEvent`](https://developer.mozilla.org/en-US/docs/Web/API/ProgressEvent) whose `loaded` property is between 0 and 1, and whose `total` property is always 1. (The exact number of total or downloaded bytes are not exposed; see the discussion in [issue #15](https://github.com/webmachinelearning/writing-assistance-apis/issues/15).)

At least two events, with `e.loaded === 0` and `e.loaded === 1`, will always be fired. This is true even if creating the model doesn't require any downloading.

<details>
<summary>What's up with this pattern?</summary>

This pattern is a little involved. Several alternatives have been considered. However, asking around the web standards community it seemed like this one was best, as it allows using standard event handlers and `ProgressEvent`s, and also ensures that once the promise is settled, the translator or language detector object is completely ready to use.
This pattern is a little involved. Several alternatives have been considered. However, asking around the web standards community it seemed like this one was best, as it allows using standard event handlers and `ProgressEvent`s, and also ensures that once the promise is settled, the returned object is completely ready to use.

It is also nicely future-extensible by adding more events and properties to the `m` object.

Expand Down
116 changes: 106 additions & 10 deletions index.bs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ urlPrefix: https://tc39.es/ecma402/; spec: ECMA-402
text: LookupMatchingLocaleByBestFit; url: sec-lookupmatchinglocalebybestfit
text: IsStructurallyValidLanguageTag; url: sec-isstructurallyvalidlanguagetag
text: CanonicalizeUnicodeLocaleId; url: sec-canonicalizeunicodelocaleid
urlPrefix: https://tc39.es/ecma262/; spec: ECMA-262
type: abstract-op
text: floor; url: eqn-floor
</pre>

<style>
Expand Down Expand Up @@ -191,13 +194,15 @@ The <dfn attribute for="AI">summarizer</dfn> getter steps are to return [=this=]

If this throws an exception |e|, catch it, and return [=a promise rejected with=] |e|.

1. Set |fireProgressEvent| to an algorithm taking arguments |loaded| and |total|, which performs the following steps:
1. Set |fireProgressEvent| to an algorithm taking argument |loaded|, which performs the following steps:

1. [=Assert=]: this algorithm is running [=in parallel=].

1. [=Queue a global task=] on the [=AI task source=] given [=this=]'s [=relevant global object=] to perform the following steps:

1. [=Fire an event=] named {{AICreateMonitor/downloadprogress}} at |monitor|, using {{ProgressEvent}}, with the {{ProgressEvent/loaded}} attribute initialized to |loaded|, the {{ProgressEvent/total}} attribute initialized to |total|, and the {{ProgressEvent/lengthComputable}} attribute initialized to true.
1. [=Fire an event=] named {{AICreateMonitor/downloadprogress}} at |monitor|, using {{ProgressEvent}}, with the {{ProgressEvent/loaded}} attribute initialized to |loaded|, the {{ProgressEvent/total}} attribute initialized to 1, and the {{ProgressEvent/lengthComputable}} attribute initialized to true.

<p class="advisement">This assumes <a href="https://github.com/whatwg/xhr/pull/394">whatwg/xhr#394</a> is merged so that passing non-integer values for {{ProgressEvent/loaded}} works as expected.</p>

1. Let |abortedDuringDownload| be false.

Expand Down Expand Up @@ -234,13 +239,9 @@ The <dfn attribute for="AI">summarizer</dfn> getter steps are to return [=this=]
::
1. If [=initializing the summarization model=] given |promise| and |options| returns false, then abort these steps.

1. Let |totalBytes| be the total size of the previously-downloaded summarization capabilities, in bytes.

1. [=Assert=]: |totalBytes| is greater than 0.
1. Perform |fireProgressEvent| given 0.

1. Perform |fireProgressEvent| given 0 and |totalBytes|.

1. Perform |fireProgressEvent| given |totalBytes| and |totalBytes|.
1. Perform |fireProgressEvent| given 1.

1. [=Finalize summarizer creation=] given |promise| and |options|.

Expand All @@ -254,7 +255,7 @@ The <dfn attribute for="AI">summarizer</dfn> getter steps are to return [=this=]

1. Let |lastProgressTime| be the [=monotonic clock=]'s [=monotonic clock/unsafe current time=].

1. Perform |fireProgressEvent| given 0 and |totalBytes|.
1. Perform |fireProgressEvent| given 0.

1. While true:

Expand All @@ -266,10 +267,105 @@ The <dfn attribute for="AI">summarizer</dfn> getter steps are to return [=this=]

1. [=Assert=]: |bytesSoFar| is greater than 0 and less than or equal to |totalBytes|.

1. Perform |fireProgressEvent| given |bytesSoFar| and |totalBytes|.
1. Let |rawProgressFraction| be |bytesSoFar| divided by |totalBytes|.

1. Let |progressFraction| be [$floor$](|rawProgressFraction| &times; 65,536) &divide; 65,536.

1. Perform |fireProgressEvent| given |progressFraction|.

<div class="note">
<p>We use a fraction, instead of firing a progress event with the number of bytes downloaded, to avoid giving precise information about the size of the model or other material being downloaded.</p>

<p>|progressFraction| is calculated from |rawProgressFraction| to give a precision of one part in 2<sup>16</sup>. This ensures that over most internet speeds and with most model sizes, the {{ProgressEvent/loaded}} value will be different from the previous one that was fired ~50 milliseconds ago.</p>

<details>
<summary>Full calculation</summary>

<p>Assume a 5 GiB download size, and a 20 Mbps download speed (chosen as a number on the lower range from [this source](https://worldpopulationreview.com/country-rankings/internet-speeds-by-country)). Then, downloading 5 GiB will take:</p>

<math style="display:block math">
<mtable>
<mtr>
<mtd></mtd>
<mtd style="text-align: left">
<mn>5</mn>
<mtext>&nbsp;GiB</mtext>

<mo>×</mo>
<mfrac>
<mrow>
<msup>
<mn>2</mn>
<mn>30</mn>
</msup>
<mtext>&nbsp;bytes</mtext>
</mrow>
<mtext>GiB</mtext>
</mfrac>

<mo>×</mo>
<mfrac>
<mrow>
<mn>8</mn>
<mtext>&nbsp;bits</mtext>
</mrow>
<mtext>bytes</mtext>
</mfrac>

<mo>÷</mo>
<mfrac>
<mrow>
<mn>20</mn>
<mo>×</mo>
<msup>
<mn>10</mn>
<mn>6</mn>
</msup>
<mtext>&nbsp;bits</mtext>
</mrow>
<mtext>s</mtext>
</mfrac>

<mo>×</mo>
<mfrac>
<mrow>
<mn>1000</mn>
<mtext>&nbsp;ms</mtext>
</mrow>
<mtext>s</mtext>
</mfrac>

<mo>÷</mo>
<mfrac>
<mrow>
<mn>50</mn>
<mtext>&nbsp;ms</mtext>
</mrow>
<mtext>interval</mtext>
</mfrac>
</mtd>
</mtr>

<mtr>
<mtd>
<mo>=</mo>
</mtd>
<mtd style="text-align: left">
<mn>49,950</mn>
<mtext>&nbsp;intervals</mtext>
</mtd>
</mtr>
</mtable>
</math>

Rounding up to the nearest power of two gives a conservative estimate of 65,536 fifty millisecond intervals, so we want to give progress to 1 part in 2<sup>16</sup>.
</details>
</div>

1. If |bytesSoFar| equals |totalBytes|, then [=iteration/break=].

<p class="note">Since this is the only exit condition for the loop, we are guaranteed to fire a {{AICreateMonitor/downloadprogress}} event for the 100% mark.</p>

1. Set |lastProgressTime| to the [=monotonic clock=]'s [=monotonic clock/unsafe current time=].

1. Otherwise, if downloading has failed and cannot continue, then:
Expand Down
Loading