Skip to content

Commit

Permalink
Change progress to be between 0 and 1
Browse files Browse the repository at this point in the history
Closes #15.

Depends on whatwg/xhr#394.
  • Loading branch information
domenic committed Jan 22, 2025
1 parent da9ac67 commit c09c6e3
Show file tree
Hide file tree
Showing 2 changed files with 114 additions and 14 deletions.
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

0 comments on commit c09c6e3

Please sign in to comment.