Repository: probabl-ai/skore

Issue:
BUG: TableReport in skore UI
### Describe the bug

Display of `CramÃ©r's V` and `â` (and some other stuff like that) of a skrub TableReport in the skore UI:

<img width="1047" alt="Capture d’écran 2024-12-03 à 10 30 12" src="https://github.com/user-attachments/assets/14ff6e08-d057-4704-80e0-8f21143e1cc2">


### Steps/Code to Reproduce

```python
from skrub.datasets import fetch_employee_salaries
from skrub import TableReport

dataset = fetch_employee_salaries()
employees_df, salaries = dataset.X, dataset.y
my_tablereport = TableReport(employees_df)
my_project.put("my_tablereport", my_tablereport)
my_tablereport
```

### Environment

```shell
System:
    python: 3.12.4 | packaged by Anaconda, Inc. | (main, Jun 18 2024, 10:07:17) [Clang 14.0.6 ]
executable: /Users/sylvaincom/Documents/GitHub/skore/.venv/bin/python
   machine: macOS-15.1.1-arm64-arm-64bit

Python dependencies:
        skore: 0.0.0+unknown
          pip: 24.3.1
   setuptools: None
    diskcache: 5.6.3
      fastapi: 0.115.3
 plotly<6,>=5: None
      pyarrow: 18.1.0
         rich: 13.9.3
        skops: 0.10.0
      uvicorn: 0.32.0
```

Discussion:

Patch:
diff --git a/skore/src/skore/ui/project_routes.py b/skore/src/skore/ui/project_routes.py
index f74b076..82340c8 100644
--- a/skore/src/skore/ui/project_routes.py
+++ b/skore/src/skore/ui/project_routes.py
@@ -78,8 +78,12 @@ def __serialize_project(project: Project) -> SerializedProject:
                 value = item.estimator_html_repr
                 media_type = "application/vnd.sklearn.estimator+html"
             elif isinstance(item, MediaItem):
-                value = base64.b64encode(item.media_bytes).decode()
-                media_type = f"{item.media_type};base64"
+                if "text" in item.media_type:
+                    value = item.media_bytes.decode(encoding=item.media_encoding)
+                    media_type = f"{item.media_type}"
+                else:
+                    value = base64.b64encode(item.media_bytes).decode()
+                    media_type = f"{item.media_type};base64"
             elif isinstance(
                 item, (CrossValidationItem, CrossValidationAggregationItem)
             ):


Test patch:
diff --git a/skore/tests/integration/ui/test_ui.py b/skore/tests/integration/ui/test_ui.py
index 7a569fb..f538e59 100644
--- a/skore/tests/integration/ui/test_ui.py
+++ b/skore/tests/integration/ui/test_ui.py
@@ -1,7 +1,11 @@
+import numpy
 import pandas
 import polars
 import pytest
 from fastapi.testclient import TestClient
+from PIL import Image
+from sklearn.linear_model import Lasso
+from skore.item.media_item import MediaItem
 from skore.ui.app import create_app
 from skore.view.view import View
 
@@ -98,3 +102,42 @@ def test_serialize_polars_series_with_missing_values(client, in_memory_project):
     assert response.status_code == 200
     project = response.json()
     assert len(project["items"]["🐻‍❄️"][0]["value"]) == 6
+
+
+def test_serialize_numpy_array(client, in_memory_project):
+    np_array = numpy.array([1, 2, 3, 4])
+    in_memory_project.put("np array", np_array)
+
+    response = client.get("/api/project/items")
+    assert response.status_code == 200
+    project = response.json()
+    assert len(project["items"]["np array"][0]["value"]) == 4
+
+
+def test_serialize_sklearn_estimator(client, in_memory_project):
+    estimator = Lasso()
+    in_memory_project.put("estimator", estimator)
+
+    response = client.get("/api/project/items")
+    assert response.status_code == 200
+    project = response.json()
+    assert project["items"]["estimator"][0]["value"] is not None
+
+
+def test_serialize_media_item(client, in_memory_project):
+    imarray = numpy.random.rand(100, 100, 3) * 255
+    img = Image.fromarray(imarray.astype("uint8")).convert("RGBA")
+    in_memory_project.put("img", img)
+
+    html = "<h1>éàªªUœALDXIWDŸΩΩ</h1>"
+    in_memory_project.put("html", html)
+    in_memory_project.put_item(
+        "media html", MediaItem.factory_str(html, media_type="text/html")
+    )
+
+    response = client.get("/api/project/items")
+    assert response.status_code == 200
+    project = response.json()
+    assert "image" in project["items"]["img"][0]["media_type"]
+    assert project["items"]["html"][0]["value"] == html
+    assert project["items"]["media html"][0]["value"] == html