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

[Widgets] Refactor examples running #1023

Merged
merged 27 commits into from
Oct 30, 2023
Merged

[Widgets] Refactor examples running #1023

merged 27 commits into from
Oct 30, 2023

Conversation

mishig25
Copy link
Collaborator

@mishig25 mishig25 commented Oct 13, 2023

Refactor codes related to running widget examples

Overall, this refactor cleans some code & adds more usage of typing.

  1. Replaces getDemoInputs with getWidgetExample function as proposed in Proposal: Slightly better than getDemoInputs already #1021
  2. Removes previewInputSample functions entirely by re-using applyInputSample function with new isPreview opts. (See more here)

change signature of

function applyInputSample(sample: WidgetExample)

to

function applyInputSample(sample: WidgetExample, opts: {isPreview?: boolean, inferenceOpts?: InferenceRunOpts; })

so that applyInputSample fully replaces previewInputSample when used with opts isPreview=true and applyInputSample with getWidgetExample replaces getDemoInputs and running example code in onMount. (in this example, I've used AudioClassificationWidget.svelte but the changes apply to all widgets).

Order of merge operations

  1. Merge [Widgets] Refactor examples running #1023 (this PR)
  2. Then merge [Widgets] Don't call api inference if output exists #1063
  3. Then merge [Widgets] Handle display logic #1019

@mishig25 mishig25 changed the title Refactor_examples_running Refactor examples running Oct 13, 2023
@mishig25 mishig25 changed the title Refactor examples running [Widgets] Refactor examples running Oct 15, 2023
@mishig25 mishig25 force-pushed the refactor_examples_running branch from 474d080 to d5325f9 Compare October 16, 2023 08:59
@mishig25 mishig25 marked this pull request as ready for review October 16, 2023 08:59
fileUrl = src;
selectedSampleUrl = src;
getOutput({ isOnLoadCall: true });
const example = getWidgetExample<WidgetExampleAssetInput<WidgetExampleOutputLabels>>(model, validateExample);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am wondering if this logic shouldn't live in WidgetWrapper instead?
Given it's the same for every widget

Copy link
Collaborator Author

@mishig25 mishig25 Oct 16, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think putting this logic in WidgetWrapper onMount would be confusing.
Here is the reason: all widgets except the TextGeneration & Text2Image, have same code, which is:

if (callApiOnMount && example) {
	applyInputSample(example, { inferenceOpts: { isOnLoadCall: true } });
}

However, TextGeneration & Text2Image have custom logic. Which is:

const [textParam] = getSearchParams(["text"]);
if (textParam) {
	setTextAreaValue(textParam);
	getOutput({ useCache: true });
} else {
	const example = getWidgetExample<WidgetExampleTextInput<WidgetExampleOutputText>>(model, validateExample);
	if (example && callApiOnMount) {
		applyInputSample(example, { inferenceOpts: { isOnLoadCall: true, useCache: true } });
	}
}

Therefore, to implement it inside WidgetWrapper onMount, we would need to check if it is TextGen or Text2Image and getSearchParams, which seems too implicit & confusing.
Wdyt?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BTW is there some kind of Svelte component inheritance? does this concept exist?

Copy link
Collaborator Author

@mishig25 mishig25 Oct 16, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No. the closest thing in svelte is usage of slots as in

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe @coyotte508 can also take a quick look and let us know if you have ideas on whether to factor that logic in WidgetWrapper or not?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess we can include the onMount call in a helper and have a useExample(model, callApiOnMount, validateExample) or useExample($$props) called here.

Thought about it. I think it actually makes it look quite confusing/unclean when you have to pass 2 callback functions: validateExample & applyInputSample

so it will look like this in that case:
helpers.ts

import { onMount } from "svelte"
...
export function customOnMount<T>(model: ModelData, isOnLoadCall: boolean, validate: func, applyInputSample: func){
    onMount(() => {
        const example = getExample<T>(model, validate);
        if(example && isOnLoadCall){
            applyInputSample(example);
        }
    })
}

and in XyzWidget.svelte

import { customOnMount } from "./helpers"
...
customOnMount<WidgetOutput<T>>(model, isOnLoadCall, validateExample, applyInputSample)

If you are fine, I'm happy to merge this PR as it is and iterate afterwards

Copy link
Member

@coyotte508 coyotte508 Oct 19, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rather than customOnMount, a useXxx name would be much better (see React-Use and Vue-use, it's a known named pattern), eg useInputSample or useLoadExample

it actually makes it look quite confusing/unclean when you have to pass 2 callback functions: validateExample & applyInputSample

You could pass $$props 😬

customOnMount<WidgetOutput<T>>

There's probably a way to not have to specify <WidgetOutput<T>>

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Made an update.
All the onMount example runs are happening inside WidgetWrapper.

Specifically, there are two parts:
Part 1: run one of the examples from the model card

const example = getWidgetExample<TWidgetExample>(model, validateExample);
if (callApiOnMount && example) {
applyInputSample(example, { inferenceOpts: { isOnLoadCall: true } });
}

Part 2: run examples from URL query param (for widgets like TextGen widget as mentioned here)

if (exampleQueryParams.length) {
const example = {} as TWidgetExample;
for (const key of exampleQueryParams) {
const val = getQueryParamVal(key);
example[key] = val;
}
applyInputSample(example);

To activate, Part 2, widget (example: TextGen widget) needs to pass/activate optional prop in WudgetWrapper

@SBrandeis @coyotte508 wdyt?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

at a glance it's a lot cleaner / removes a lot of code, it's nice!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WDYT @SBrandeis ?

@mishig25
Copy link
Collaborator Author

find an updates in this comment

Copy link
Member

@coyotte508 coyotte508 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO let's pull the trigger on this PR and iterate if needed

(Nice refacto!)

Copy link
Contributor

@SBrandeis SBrandeis left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool stuff, I like the solution you've chosen
Way cleaner this way👍

Please address the few comments left before merging

export let validateExample: (sample: WidgetExample) => sample is TWidgetExample;
export let exampleQueryParams: QueryParam[] = [];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this be typed further?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Like this for example (requires some ugly casts in onMount but I think I'm fine with it as it makes it almost impossible to pass wrong query params)

Suggested change
export let exampleQueryParams: QueryParam[] = [];
export let exampleQueryParams: (keyof TWidgetExample)[] = [];

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, stronger typing is possible c89086b

Comment on lines 70 to 83
// run widget example
if (exampleQueryParams.length) {
const example = {} as TWidgetExample;
for (const key of exampleQueryParams) {
const val = getQueryParamVal(key);
example[key] = val;
}
applyInputSample(example);
} else {
const example = getWidgetExample<TWidgetExample>(model, validateExample);
if (callApiOnMount && example) {
applyInputSample(example, { inferenceOpts: { isOnLoadCall: true } });
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I'm not mistaken, getWidgetExample here is never called when exampleQueryParams is set in the concrete Widget component (ie most widgets)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok I tested it and I can confirm this behavior!

Copy link
Collaborator Author

@mishig25 mishig25 Oct 27, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there anything that needs to be changed here?

If I'm not mistaken, getWidgetExample here is never called when exampleQueryParams is set in the concrete Widget component (ie most widgets)

I think this is the existing behaviour on main

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there anything that needs to be changed here?

yes

I think this is the existing behaviour on main

No

Sorry I think my wording was not clear here. I'll try to reformulate.

On main (hf.co)

When accessing for example HuggingFaceH4/zephyr-7b-alpha, The widget picksan example at random and starts generating (expected behavior).

image

On this branch

The widget does not try to get the example from the widgetData and displays this error (bug?).
This is because exampleQueryParams.length is always truthy in the TextGenerationWidget, so the getWidgetExample call get skipped

image


Let me know if anything remains unclear 🤗

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

indeed, fixed the logic 7ebca52

js/src/lib/components/InferenceWidget/shared/helpers.ts Outdated Show resolved Hide resolved
if (callApiOnMount && example) {
applyInputSample(example, { inferenceOpts: { isOnLoadCall: true } });
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure but isOnLoadCall should prioritize the example output when specified, no?
Probably for a subsequent PR

type QueryParamVal = string | null | boolean | (string | number)[][];
export function getQueryParamVal(key: QueryParam): QueryParamVal {
const searchParams = new URL(window.location.href).searchParams;
const value = searchParams.get(key);
if (["text", "context", "question", "query", "candidateLabels"].includes(key)) {
if (["text", "context", "question", "query", "candidate_labels"].includes(key)) {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this will not work on existing URLs of ZeroShotClassification widget with URL query candidateLabels rather than candidate_labels. However, ZeroShotClassification widgets are not used widely. Therefore, I think this breaking change is fine

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think i agree for candidate_labels (it's used in the spec for widget examples) but for structuredData it's used that way in the spec no?

https://huggingface.co/docs/hub/models-widgets-examples#structured-data-classification

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wdyt #1064?
I can either merge #1064 or revert the changes back to using strucutredData. I will let you decide

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

see my comment there

return value;
} else if (["table", "structuredData"].includes(key)) {
} else if (["table", "structured_data"].includes(key)) {
Copy link
Collaborator Author

@mishig25 mishig25 Oct 30, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same as the comment above but for structuredData vs structured_data

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mishig25 mishig25 merged commit cd15b19 into main Oct 30, 2023
2 checks passed
@mishig25 mishig25 deleted the refactor_examples_running branch October 30, 2023 11:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants