Skip to content

Export jinja files as TS module at build time #1296

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

Merged
merged 3 commits into from
Mar 19, 2025
Merged

Conversation

Wauplin
Copy link
Contributor

@Wauplin Wauplin commented Mar 19, 2025

This PR should fix https://github.com/huggingface-internal/moon-landing/pull/13013. It partially removes the structure introduced in #1255.

Current problem is that inference snippets are generated in the front-end. Since front-end cannot access file-system and therefore the jinja files, we have to find a workaround. This PR adds a build step which exports all jinja files into a single TS module. I've updated the package.json file so that now the snippets code should be available in any environment (both node and browser).

cc @coyotte508 who suggested such a solution.

Tested it in @tasks-gen and "it works"


For the record, the exported file (not committed in this PR) looks like this:

# packages/inference/src/snippets/templates.exported.ts

// Generated file - do not edit directly
export const templates: Record<string, Record<string, Record<string, string>>> = {
  "js": {
    "fetch": {
      "basic": "async function query(data) {\n\tconst response = await fetch(\n\t\t\"{{ fullUrl }}\",\n\t\t{\n\t\t\theaders: {\n\t\t\t\tAuthorization: \"{{ authorizationHeader }}\",\n\t\t\t\t\"Content-Type\": \"application/json\",\n\t\t\t},\n\t\t\tmethod: \"POST\",\n\t\t\tbody: JSON.stringify(data),\n\t\t}\n\t);\n\tconst result = await response.json();\n\treturn result;\n}\n\nquery({ inputs: {{ providerInputs.asObj.inputs }} }).then((response) => {\n    console.log(JSON.stringify(response));\n});",
      "basicAudio": "async function query(data) {\n\tconst response = await fetch(\n\t\t\"{{ fullUrl }}\",\n\t\t{\n\t\t\theaders: {\n\t\t\t\tAuthorization: \"{{ authorizationHeader }}\",\n\t\t\t\t\"Content-Type\": \"audio/flac\"\n\t\t\t},\n\t\t\tmethod: \"POST\",\n\t\t\tbody: JSON.stringify(data),\n\t\t}\n\t);\n\tconst result = await response.json();\n\treturn result;\n}\n\nquery({ inputs: {{ providerInputs.asObj.inputs }} }).then((response) => {\n    console.log(JSON.stringify(response));\n});",
      "basicImage": "async function query(data) {\n\tconst response = await fetch(\n\t\t\"{{ fullUrl }}\",\n\t\t{\n\t\t\theaders: {\n\t\t\t\tAuthorization: \"{{ authorizationHeader }}\",\n\t\t\t\t\"Content-Type\": \"image/jpeg\"\n\t\t\t},\n\t\t\tmethod: \"POST\",\n\t\t\tbody: JSON.stringify(data),\n\t\t}\n\t);\n\tconst result = await response.json();\n\treturn result;\n}\n\nquery({ inputs: {{ providerInputs.asObj.inputs }} }).then((response) => {\n    console.log(JSON.stringify(response));\n});",
      "textToAudio": "{% if model.library_name == \"transformers\" %}\nasync function query(data) {\n\tconst response = await fetch(\n\t\t\"{{ fullUrl }}\",\n\t\t{\n\t\t\theaders: {\n\t\t\t\tAuthorization: \"{{ authorizationHeader }}\",\n\t\t\t\t\"Content-Type\": \"application/json\",\n\t\t\t},\n\t\t\tmethod: \"POST\",\n\t\t\tbody: JSON.stringify(data),\n\t\t}\n\t);\n\tconst result = await response.blob();\n    return result;\n}\n\nquery({ inputs: {{ providerInputs.asObj.inputs }} }).then((response) => {\n    // Returns a byte object of the Audio wavform. Use it directly!\n});\n{% else %}\nasync function query(data) {\n\tconst response = await fetch(\n\t\t\"{{ fullUrl }}\",\n\t\t{\n\t\t\theaders: {\n\t\t\t\tAuthorization: \"{{ authorizationHeader }}\",\n\t\t\t\t\"Content-Type\": \"application/json\",\n\t\t\t},\n\t\t\tmethod: \"POST\",\n\t\t\tbody: JSON.stringify(data),\n\t\t}\n\t);\n    const result = await response.json();\n    return result;\n}\n\nquery({ inputs: {{ providerInputs.asObj.inputs }} }).then((response) => {\n    console.log(JSON.stringify(response));\n});\n{% endif %} ",
      "textToImage": "async function query(data) {\n\tconst response = await fetch(\n\t\t\"{{ fullUrl }}\",\n\t\t{\n\t\t\theaders: {\n\t\t\t\tAuthorization: \"{{ authorizationHeader }}\",\n\t\t\t\t\"Content-Type\": \"application/json\",\n\t\t\t},\n\t\t\tmethod: \"POST\",\n\t\t\tbody: JSON.stringify(data),\n\t\t}\n\t);\n\tconst result = await response.blob();\n\treturn result;\n}\n\nquery({ inputs: {{ providerInputs.asObj.inputs }} }).then((response) => {\n    // Use image\n});",
      "zeroShotClassification": "async function query(data) {\n    const response = await fetch(\n\t\t\"{{ fullUrl }}\",\n        {\n            headers: {\n\t\t\t\tAuthorization: \"{{ authorizationHeader }}\",\n                \"Content-Type\": \"application/json\",\n            },\n            method: \"POST\",\n            body: JSON.stringify(data),\n        }\n    );\n    const result = await response.json();\n    return result;\n}\n\nquery({\n    inputs: {{ providerInputs.asObj.inputs }},\n    parameters: { candidate_labels: [\"refund\", \"legal\", \"faq\"] }\n}).then((response) => {\n    console.log(JSON.stringify(response));\n});"
    },
    "huggingface.js": {
      "basic": "import { InferenceClient } from \"@huggingface/inference\";\n\nconst client = new InferenceClient(\"{{ accessToken }}\");\n\nconst output = await client.{{ methodName }}({\n\tmodel: \"{{ model.id }}\",\n\tinputs: {{ inputs.asObj.inputs }},\n\tprovider: \"{{ provider }}\",\n});\n\nconsole.log(output);",
      "basicAudio": "import { InferenceClient } from \"@huggingface/inference\";\n\nconst client = new InferenceClient(\"{{ accessToken }}\");\n\nconst data = fs.readFileSync({{inputs.asObj.inputs}});\n\nconst output = await client.{{ methodName }}({\n\tdata,\n\tmodel: \"{{ model.id }}\",\n\tprovider: \"{{ provider }}\",\n});\n\nconsole.log(output);",
      "basicImage": "import { InferenceClient } from \"@huggingface/inference\";\n\nconst client = new InferenceClient(\"{{ accessToken }}\");\n\nconst data = fs.readFileSync({{inputs.asObj.inputs}});\n\nconst output = await client.{{ methodName }}({\n\tdata,\n\tmodel: \"{{ model.id }}\",\n\tprovider: \"{{ provider }}\",\n});\n\nconsole.log(output);",
      "conversational": "import { InferenceClient } from \"@huggingface/inference\";\n\nconst client = new InferenceClient(\"{{ accessToken }}\");\n\nconst chatCompletion = await client.chatCompletion({\n    provider: \"{{ provider }}\",\n    model: \"{{ model.id }}\",\n{{ inputs.asTsString }}\n});\n\nconsole.log(chatCompletion.choices[0].message);",
      "conversationalStream": "import { InferenceClient } from \"@huggingface/inference\";\n\nconst client = new InferenceClient(\"{{ accessToken }}\");\n\nlet out = \"\";\n\nconst stream = await client.chatCompletionStream({\n    provider: \"{{ provider }}\",\n    model: \"{{ model.id }}\",\n{{ inputs.asTsString }}\n});\n\nfor await (const chunk of stream) {\n\tif (chunk.choices && chunk.choices.length > 0) {\n\t\tconst newContent = chunk.choices[0].delta.content;\n\t\tout += newContent;\n\t\tconsole.log(newContent);\n\t}  \n}",
      "textToImage": "import { InferenceClient } from \"@huggingface/inference\";\n\nconst client = new InferenceClient(\"{{ accessToken }}\");\n\nconst image = await client.textToImage({\n    provider: \"{{ provider }}\",\n    model: \"{{ model.id }}\",\n\tinputs: {{ inputs.asObj.inputs }},\n\tparameters: { num_inference_steps: 5 },\n});\n/// Use the generated image (it's a Blob)",
      "textToVideo": "import { InferenceClient } from \"@huggingface/inference\";\n\nconst client = new InferenceClient(\"{{ accessToken }}\");\n\nconst image = await client.textToVideo({\n    provider: \"{{ provider }}\",\n    model: \"{{ model.id }}\",\n\tinputs: {{ inputs.asObj.inputs }},\n});\n// Use the generated video (it's a Blob)"
    },
    "openai": {
      "conversational": "import { OpenAI } from \"openai\";\n\nconst client = new OpenAI({\n\tbaseURL: \"{{ baseUrl }}\",\n\tapiKey: \"{{ accessToken }}\",\n});\n\nconst chatCompletion = await client.chat.completions.create({\n\tmodel: \"{{ providerModelId }}\",\n{{ inputs.asTsString }}\n});\n\nconsole.log(chatCompletion.choices[0].message);",
      "conversationalStream": "import { OpenAI } from \"openai\";\n\nconst client = new OpenAI({\n\tbaseURL: \"{{ baseUrl }}\",\n\tapiKey: \"{{ accessToken }}\",\n});\n\nlet out = \"\";\n\nconst stream = await client.chat.completions.create({\n    provider: \"{{ provider }}\",\n    model: \"{{ model.id }}\",\n{{ inputs.asTsString }}\n});\n\nfor await (const chunk of stream) {\n\tif (chunk.choices && chunk.choices.length > 0) {\n\t\tconst newContent = chunk.choices[0].delta.content;\n\t\tout += newContent;\n\t\tconsole.log(newContent);\n\t}  \n}"
    }
  },
  "python": {
    "fal_client": {
      "textToImage": "{% if provider == \"fal-ai\" %}\nimport fal_client\n\nresult = fal_client.subscribe(\n    \"{{ providerModelId }}\",\n    arguments={\n        \"prompt\": {{ inputs.asObj.inputs }},\n    },\n)\nprint(result)\n{% endif %} "
    },
    "huggingface_hub": {
      "basic": "result = client.{{ methodName }}(\n    inputs={{ inputs.asObj.inputs }},\n    model=\"{{ model.id }}\",\n)",
      "basicAudio": "output = client.{{ methodName }}({{ inputs.asObj.inputs }}, model=\"{{ model.id }}\")",
      "basicImage": "output = client.{{ methodName }}({{ inputs.asObj.inputs }}, model=\"{{ model.id }}\")",
      "conversational": "completion = client.chat.completions.create(\n    model=\"{{ model.id }}\",\n{{ inputs.asPythonString }}\n)\n\nprint(completion.choices[0].message) ",
      "conversationalStream": "stream = client.chat.completions.create(\n    model=\"{{ model.id }}\",\n{{ inputs.asPythonString }}\n    stream=True,\n)\n\nfor chunk in stream:\n    print(chunk.choices[0].delta.content, end=\"\") ",
      "documentQuestionAnswering": "output = client.document_question_answering(\n    \"{{ inputs.asObj.image }}\",\n    question=\"{{ inputs.asObj.question }}\",\n    model=\"{{ model.id }}\",\n) ",
      "imageToImage": "# output is a PIL.Image object\nimage = client.image_to_image(\n    \"{{ inputs.asObj.inputs }}\",\n    prompt=\"{{ inputs.asObj.parameters.prompt }}\",\n    model=\"{{ model.id }}\",\n) ",
      "importInferenceClient": "from huggingface_hub import InferenceClient\n\nclient = InferenceClient(\n    provider=\"{{ provider }}\",\n    api_key=\"{{ accessToken }}\",\n)",
      "textToImage": "# output is a PIL.Image object\nimage = client.text_to_image(\n    {{ inputs.asObj.inputs }},\n    model=\"{{ model.id }}\",\n) ",
      "textToVideo": "video = client.text_to_video(\n    {{ inputs.asObj.inputs }},\n    model=\"{{ model.id }}\",\n) "
    },
    "openai": {
      "conversational": "from openai import OpenAI\n\nclient = OpenAI(\n    base_url=\"{{ baseUrl }}\",\n    api_key=\"{{ accessToken }}\"\n)\n\ncompletion = client.chat.completions.create(\n    model=\"{{ providerModelId }}\",\n{{ inputs.asPythonString }}\n)\n\nprint(completion.choices[0].message) ",
      "conversationalStream": "from openai import OpenAI\n\nclient = OpenAI(\n    base_url=\"{{ baseUrl }}\",\n    api_key=\"{{ accessToken }}\"\n)\n\nstream = client.chat.completions.create(\n    model=\"{{ providerModelId }}\",\n{{ inputs.asPythonString }}\n    stream=True,\n)\n\nfor chunk in stream:\n    print(chunk.choices[0].delta.content, end=\"\")"
    },
    "requests": {
      "basic": "def query(payload):\n    response = requests.post(API_URL, headers=headers, json=payload)\n    return response.json()\n\noutput = query({\n    \"inputs\": {{ providerInputs.asObj.inputs }},\n}) ",
      "basicAudio": "def query(filename):\n    with open(filename, \"rb\") as f:\n        data = f.read()\n    response = requests.post(API_URL, headers={\"Content-Type\": \"audio/flac\", **headers}, data=data)\n    return response.json()\n\noutput = query({{ providerInputs.asObj.inputs }})",
      "basicImage": "def query(filename):\n    with open(filename, \"rb\") as f:\n        data = f.read()\n    response = requests.post(API_URL, headers={\"Content-Type\": \"image/jpeg\", **headers}, data=data)\n    return response.json()\n\noutput = query({{ providerInputs.asObj.inputs }})",
      "conversational": "def query(payload):\n    response = requests.post(API_URL, headers=headers, json=payload)\n    return response.json()\n\nresponse = query({\n{{ providerInputs.asJsonString }}\n})\n\nprint(response[\"choices\"][0][\"message\"])",
      "conversationalStream": "def query(payload):\n    response = requests.post(API_URL, headers=headers, json=payload, stream=True)\n    for line in response.iter_lines():\n        if not line.startswith(b\"data:\"):\n            continue\n        if line.strip() == b\"data: [DONE]\":\n            return\n        yield json.loads(line.decode(\"utf-8\").lstrip(\"data:\").rstrip(\"/n\"))\n\nchunks = query({\n{{ providerInputs.asJsonString }},\n    \"stream\": True,\n})\n\nfor chunk in chunks:\n    print(chunk[\"choices\"][0][\"delta\"][\"content\"], end=\"\")",
      "documentQuestionAnswering": "def query(payload):\n    with open(payload[\"image\"], \"rb\") as f:\n        img = f.read()\n        payload[\"image\"] = base64.b64encode(img).decode(\"utf-8\")\n    response = requests.post(API_URL, headers=headers, json=payload)\n    return response.json()\n\noutput = query({\n    \"inputs\": {\n        \"image\": \"{{ inputs.asObj.image }}\",\n        \"question\": \"{{ inputs.asObj.question }}\",\n    },\n}) ",
      "imageToImage": "def query(payload):\n    with open(payload[\"inputs\"], \"rb\") as f:\n        img = f.read()\n        payload[\"inputs\"] = base64.b64encode(img).decode(\"utf-8\")\n    response = requests.post(API_URL, headers=headers, json=payload)\n    return response.content\n\nimage_bytes = query({\n{{ providerInputs.asJsonString }}\n})\n\n# You can access the image with PIL.Image for example\nimport io\nfrom PIL import Image\nimage = Image.open(io.BytesIO(image_bytes)) ",
      "importRequests": "{% if importBase64 %}\nimport base64\n{% endif %}\n{% if importJson %}\nimport json\n{% endif %}\nimport requests\n\nAPI_URL = \"{{ fullUrl }}\"\nheaders = {\"Authorization\": \"{{ authorizationHeader }}\"}",
      "tabular": "def query(payload):\n    response = requests.post(API_URL, headers=headers, json=payload)\n    return response.content\n\nresponse = query({\n    \"inputs\": {\n        \"data\": {{ providerInputs.asObj.inputs }}\n    },\n}) ",
      "textToAudio": "{% if model.library_name == \"transformers\" %}\ndef query(payload):\n    response = requests.post(API_URL, headers=headers, json=payload)\n    return response.content\n\naudio_bytes = query({\n    \"inputs\": {{ providerInputs.asObj.inputs }},\n})\n# You can access the audio with IPython.display for example\nfrom IPython.display import Audio\nAudio(audio_bytes)\n{% else %}\ndef query(payload):\n    response = requests.post(API_URL, headers=headers, json=payload)\n    return response.json()\n\naudio, sampling_rate = query({\n    \"inputs\": {{ providerInputs.asObj.inputs }},\n})\n# You can access the audio with IPython.display for example\nfrom IPython.display import Audio\nAudio(audio, rate=sampling_rate)\n{% endif %} ",
      "textToImage": "{% if provider == \"hf-inference\" %}\ndef query(payload):\n    response = requests.post(API_URL, headers=headers, json=payload)\n    return response.content\n\nimage_bytes = query({\n    \"inputs\": {{ providerInputs.asObj.inputs }},\n})\n\n# You can access the image with PIL.Image for example\nimport io\nfrom PIL import Image\nimage = Image.open(io.BytesIO(image_bytes))\n{% endif %}",
      "zeroShotClassification": "def query(payload):\n    response = requests.post(API_URL, headers=headers, json=payload)\n    return response.json()\n\noutput = query({\n    \"inputs\": {{ providerInputs.asObj.inputs }},\n    \"parameters\": {\"candidate_labels\": [\"refund\", \"legal\", \"faq\"]},\n}) ",
      "zeroShotImageClassification": "def query(data):\n    with open(data[\"image_path\"], \"rb\") as f:\n        img = f.read()\n    payload={\n        \"parameters\": data[\"parameters\"],\n        \"inputs\": base64.b64encode(img).decode(\"utf-8\")\n    }\n    response = requests.post(API_URL, headers=headers, json=payload)\n    return response.json()\n\noutput = query({\n    \"image_path\": {{ providerInputs.asObj.inputs }},\n    \"parameters\": {\"candidate_labels\": [\"cat\", \"dog\", \"llama\"]},\n}) "
    }
  },
  "sh": {
    "curl": {
      "basic": "curl {{ fullUrl }} \\\n    -X POST \\\n    -H 'Authorization: {{ authorizationHeader }}' \\\n    -H 'Content-Type: application/json' \\\n    -d '{\n{{ providerInputs.asCurlString }}\n    }'",
      "basicAudio": "curl {{ fullUrl }} \\\n    -X POST \\\n    -H 'Authorization: {{ authorizationHeader }}' \\\n    -H 'Content-Type: audio/flac' \\\n    --data-binary @{{ providerInputs.asObj.inputs }}",
      "basicImage": "curl {{ fullUrl }} \\\n    -X POST \\\n    -H 'Authorization: {{ authorizationHeader }}' \\\n    -H 'Content-Type: image/jpeg' \\\n    --data-binary @{{ providerInputs.asObj.inputs }}",
      "conversational": "curl {{ fullUrl }} \\\n    -H 'Authorization: {{ authorizationHeader }}' \\\n    -H 'Content-Type: application/json' \\\n    -d '{\n{{ providerInputs.asCurlString }},\n        \"stream\": false\n    }'",
      "conversationalStream": "curl {{ fullUrl }} \\\n    -H 'Authorization: {{ authorizationHeader }}' \\\n    -H 'Content-Type: application/json' \\\n    -d '{\n{{ providerInputs.asCurlString }},\n        \"stream\": true\n    }'",
      "zeroShotClassification": "curl {{ fullUrl }} \\\n    -X POST \\\n    -d '{\"inputs\": {{ providerInputs.asObj.inputs }}, \"parameters\": {\"candidate_labels\": [\"refund\", \"legal\", \"faq\"]}}' \\\n    -H 'Content-Type: application/json' \\\n    -H 'Authorization: {{ authorizationHeader }}'"
    }
  }
} as const;

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.

you can probably revert the tsup changes from the previous PR? (eg the tsup.config.ts file)

Copy link
Member

@coyotte508 coyotte508 Mar 19, 2025

Choose a reason for hiding this comment

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

It could still be committed

Copy link
Contributor Author

@Wauplin Wauplin Mar 19, 2025

Choose a reason for hiding this comment

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

I feel that committing it would not bring much value but could be annoying in case of merge conflicts etc. (and adds a bigger diff in PRs)

Copy link
Member

Choose a reason for hiding this comment

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

hmm yeah i'd rather ignore it

@Wauplin
Copy link
Contributor Author

Wauplin commented Mar 19, 2025

Thanks for the quick review. I've pushed 8854b03 to remove the previously added tsup.config.ts file. Will merge it now.

@Wauplin Wauplin merged commit e8964e1 into main Mar 19, 2025
5 checks passed
@Wauplin Wauplin deleted the remove-jinja-raw-files branch March 19, 2025 13:51
@Wauplin
Copy link
Contributor Author

Wauplin commented Mar 19, 2025

cc @xenova btw. FYI this PR is a follow-up of this discussion #1255 (comment). We now export all jinja files into JS strings exported at build time. If jinja.js ever implements a jinja.compile equivalent to turn jinja templates into compiled code, that would be awesome as it would allow us to turn jinja.js from a runtime dependency to a dev dependency. Not a priority though, current solution is enough for now :)
(suggested by Julien and Eliott in some private DMs)

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.

3 participants