├── .gitignore
├── .python-version
├── AZURE_HOWTO.md
├── LICENSE
├── README.md
├── _imgs
├── aoai_create_deployment.png
├── aoai_create_deployment2.png
├── aoai_create_service.png
├── aoai_endpoint_and_key.png
├── aoai_gear_icon.png
├── aoai_goto_studio.png
├── aoai_select_service.png
├── aoai_select_service2.png
├── app_ui.png
└── zix_scores.jpg
├── _streamlit_app
├── data
│ ├── cefr_vocab.parq
│ ├── ridge_regressor.pkl
│ ├── standard_scaler.pkl
│ └── word_scores_final_0728.parq
├── sprache-vereinfachen.py
├── sprache-vereinfachen_azure.py
├── sprache-vereinfachen_google.py
├── sprache-vereinfachen_openai.py
├── utils_expander.md
├── utils_expander_google.md
├── utils_expander_openai.md
├── utils_prompts.py
├── utils_sample_texts.py
├── utils_understandability.py
└── zix_scores.jpg
├── main.py
├── pyproject.toml
├── requirements.txt
└── uv.lock
/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | _streamlit_app/.DS_Store
3 | _streamlit_app/.env
4 | *.pyc
5 | *.log
6 | .DS_Store
7 | .env
--------------------------------------------------------------------------------
/.python-version:
--------------------------------------------------------------------------------
1 | 3.12
2 |
--------------------------------------------------------------------------------
/AZURE_HOWTO.md:
--------------------------------------------------------------------------------
1 | # How to setup the app with Azure OpenAI Service
2 |
3 | In addition to (mostly US-hosted) API model endpoints, we created an app version that runs with `Azure OpenAI Services (AOAI)`. Find the docs here:
4 |
5 | **Benefits**
6 |
7 | - OpenAI models can be deployed within the **Swiss Azure Cloud Region** (e.g. `switzerlandnorth`) or regions within the EU (like `westeurope`).
8 | - This may help with achieving compliance and adress data residency considerations and governance.
9 | - Some good reference documents:
10 | - [Azure Product terms](https://www.microsoft.com/licensing/terms/productoffering/MicrosoftAzure/MCA#Documents)
11 | - [Cloud Services in the Swiss Public Sector (Overview page)](https://news.microsoft.com/de-ch/azure-services-in-the-swiss-public-sector/)
12 | - [Azure Services in the Public Sector in Switzerland - DE](https://news.microsoft.com/wp-content/uploads/prod/sites/418/2021/11/Cloud-Design_Azure-Services-Swiss-Public-Sector_DE.pdf)
13 |
14 | **Important information**
15 | Always ensure appropriate classification and handling of your data, especially for sensitive and/or personal data.
16 | Check with your Microsoft contact or Cloud Service Provider (Partner) representative for further questions.
17 |
18 | **Prerequisites**
19 |
20 | - An active **Azure account**. If you don't have one, click [here](https://azure.microsoft.com/free/cognitive-services).
21 | - In your account, an **Azure Subscription** that has been enabled for Azure OpenAI Services.
22 | - Apply here: (takes ~24hrs to activate)
23 |
24 | ## Walkthrough
25 |
26 | - Go to
27 | - From the Catalog, choose `Azure OpenAI service`
28 |
29 |
30 |
31 |
32 | - You need to select (or create) a new **Resource Group**, choose the Cloud **Region** and a unique service **Name** (becomes part of the endpoint url)
33 | - **Note:** Not all model versions are available in all regions, see [here](https://learn.microsoft.com/en-us/azure/ai-services/openai/concepts/models). The region can't be changed afterwards (you'll need to delete & create a new service instance in that case).
34 |
35 |
36 |
37 | - Follow the wizard to the end, and create the service.
38 | - Once the service instance is ready, find the overview and `Go to Azure OpenAI Studio`.
39 |
40 |
41 |
42 | - In the `Azure Open AI Studio`, go to `Deployments` and create a new model deployment.
43 | - **Note**: If you can't create a particular model type, that model may be unavailable in your region, or your quota may be exceeded.
44 |
45 |
46 |
47 |
48 | - Take note of the **deployment name** - this is needed later for making API calls
49 |
50 | - Now go back and find the **Endpoint** and **API Key**. There are multiple ways to find these, but one is clicking on the gear icon on top of OpenAI Studio:
51 |
52 |
53 |
54 |
55 | - Now add these credentials to your `_streamlit_app/.env` file:
56 |
57 | ```
58 | AZURE_OPENAI_ENDPOINT=....
59 | AZURE_OPENAI_API_KEY=....
60 | AZURE_OPENAI_DEPLOYMENT=....
61 | ```
62 |
63 | - Start the application (`cd _streamlit_app/ && python -m streamlit run ./sprache-vereinfachen_azure.py)
64 |
65 | **Congratulations - your good to go...** 🎉
66 |
67 | ## Known limitations
68 |
69 | - While latest OpenAI models find their way to Azure quite fast, some may be restricted to higher tiers for capacity reasons or arrive later (e.g. `GPT-4o` as of June 2024). Open a support ticket or talk to your Microsot representative if in doubt.
70 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Statistisches Amt Kanton Zürich
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Simply simplify language
2 |
3 | **Use LLMs to simplify your institutional communication. Get rid of «Behördendeutsch».**
4 |
5 | 
6 | [](https://github.com/machinelearningZH/simply-simplify-language)
7 | [](https://github.com/machinelearningZH/simply-simplify-language/stargazers)
8 | [](https://github.com/machinelearningZH/simply-simplify-language/issues)
9 | [](https://img.shields.io/github/issues-pr/machinelearningZH/simply-simplify-language)
10 | [](https://github.com/machinelearningZH/simply-simplify-language)
11 |
12 |
13 |
14 |
15 | Contents
16 |
17 | - [Usage](#usage)
18 | - [Project information](#project-information)
19 | - [What does the app do?](#what-does-the-app-do)
20 | - [What does it cost?](#what-does-it-cost)
21 | - [Our language guidelines](#our-language-guidelines)
22 | - [A couple of findings](#a-couple-of-findings)
23 | - [How does the understandability score work?](#how-does-the-understandability-score-work)
24 | - [What does the score mean?](#what-does-the-score-mean)
25 | - [Outlook](#outlook)
26 | - [Project team](#project-team)
27 | - [Contributing](#feedback-and-contributing)
28 | - [Miscellaneous](#miscellaneous)
29 |
30 |
31 |
32 | 
33 |
34 | ## Usage
35 |
36 | - You can run the app **locally**, **in the cloud** or **in a [GitHub Codespace](https://github.com/features/codespaces)**.
37 | - If you just have an [OpenAI](https://openai.com/api/) account and do not want to use other LLMs you also can run **a variant of the app that only uses OpenAI models**. However, we recommend to give the [Mistral](https://mistral.ai/), [Anthropic](https://www.anthropic.com/api) and [Google](https://cloud.google.com/docs/generative-ai) models a spin too. These models are very powerful too and we continuously achieve very good results.
38 | - We also added an app version that uses the [**Azure OpenAI Service**](https://azure.microsoft.com/en-us/products/ai-services/openai-service).
39 | - We also added an app version that only leverages the **Google Gemini models** (2.0 / 2.5 Flash and Pro).
40 |
41 | ### Run the app locally
42 |
43 | - Create a [Conda](https://conda.io/projects/conda/en/latest/index.html) environment: `conda create -n simplify python=3.9`
44 | - Activate environment: `conda activate simplify`
45 | - Clone this repo.
46 | - Change into the project directory: `cd simply-simplify-language/`
47 | - Install packages: `pip install -r requirements.txt`
48 | - Install Spacy language model: `python -m spacy download de_core_news_sm`
49 | - Create an `.env` file and input your API keys:
50 |
51 | ```
52 | OPENAI_API_KEY=sk-...
53 | ANTHROPIC_API_KEY=sk-...
54 | MISTRAL_API_KEY=KGT...
55 | GOOGLE_API_KEY=...
56 | ```
57 |
58 | - Change into app directory: `cd _streamlit_app/`
59 | - Start app: `streamlit run sprache-vereinfachen.py`
60 | - To **run the OpenAI only version** use `streamlit run sprache-vereinfachen_openai.py`.
61 | - To **run the Google Gemini only version** use `streamlit run sprache-vereinfachen_google.py`. Get your API key from here: [Google AI Studio](https://ai.google.dev/aistudio).
62 | - To **run the Azure OpenAI only version** use `streamlit run sprache-vereinfachen_azure.py`. Have a look [here to learn more about how to setup the app with Azure](AZURE_HOWTO.md).
63 |
64 | You can also initialize the repo with `uv`:
65 |
66 | ```bash
67 | pip3 install uv
68 | uv venv
69 | source .venv/bin/activate
70 | uv sync
71 | ```
72 |
73 | ### Run the app in the cloud
74 |
75 | - Instantiate a small virtual machine with the cloud provider of your choosing. Suggested size: 2 vCPUs, 2GB RAM, and an SSD with a couple of GBs are sufficient. This will set you back no more than a couple of Francs per month.
76 | - Install Conda and set up the repo and app as described above.
77 | - Recommendation: To use a proper domain and HTTPS it makes sense to install a reverse proxy. We very much like [Caddy server](https://caddyserver.com/) for this due to its simplicity and ease of installation and usage. It's also simple to request certificates – Caddy does [this automatically for you](https://caddyserver.com/docs/automatic-https).
78 |
79 | ### Run the app in a Github Codespace
80 |
81 | - This will enable you to develop and run the app in a cloud-hosted development workspace, using [GitHub Codespaces](https://docs.github.com/en/codespaces/overview).
82 | - Some benefits: No need for any local installation, you can do anything right from your Web Browser. You get some free hours with your GitHub account, so this should not be expensive at all. However, **do not forget to delete unused Codespaces to avoid being billed unnecessarily.** It's also a sensible idea, to make sure that `Auto-delete codespace` is activated in the settings.
83 | - Create a GitHub codespace on this repository by clicking `Code > Codespaces > Create codespace on main`
84 | - Wait until the codespace is started. You'll get a new url like `https://scaling-pancake-jwjjw54r4r7hpqpg.github.dev/`
85 | - If you run into network connection issues try another browser. In our testing Firefox sometimes threw errors, Chrome worked fine.
86 | - Install the project requirements from the terminal: `pip install -r requirements.txt`
87 | - Install spacy language model: `python -m spacy download de_core_news_sm`
88 | - Create an `.env` file and input your API keys like described above.
89 | - Alternatively, create Repository Secrets on GitHub, which will get available for your codespaces automatically when starting up (only if you are a repo owner / using your own fork).
90 | - Start app: `python -m streamlit run _streamlit_app/sprache-vereinfachen.py`
91 | - Codespaces auto-proxies and forwards Port 8501 to something like `https://scaling-pancake-jwjjw54r4r7hpqpg.github.dev/`
92 | - In case you don't like coding in your browser, you can also use a local [Visual Studio Code IDE](https://code.visualstudio.com/) and connect to the remote Codespace.
93 |
94 | > [!Note]
95 | > The app logs user interactions to your local computer or virtual machine to a file named `app.log`. If you do not want to have analytics, simply comment out the function call in the code.
96 |
97 | ## Project information
98 |
99 | **Institutional communication is often overly complicated and hard to understand.** This particularly affects citizens who do not speak German as their first language or who struggle with complex texts for other reasons. Clear and simple communication is essential to [ensure everyone can participate in public processes and access services equally](https://www.zh.ch/de/direktion-der-justiz-und-des-innern/schwerpunkt-teilhabe.html).
100 |
101 | For many years, the cantonal administration of Zurich has gone to great lengths to make communication more inclusive and accessible. With the increasing volume of content, we wanted to explore the potential of AI to assist in this effort. In autumn 2023, we launched a pilot project. This app is one of the results. The code in this repository represents a snapshot of our ongoing efforts.
102 |
103 | We developed this app following our communication guidelines. However, we believe it can be easily adapted for use by other public institutions.
104 |
105 | ### What does the app do?
106 |
107 | - This app **simplifies complex texts, rewriting them according to rules for [«Einfache Sprache»](https://de.wikipedia.org/wiki/Einfache_Sprache) or [«Leichte Sprache»](https://de.wikipedia.org/wiki/Leichte_Sprache)**. To simplify your source text, the app applies effective prompting, and uses your chosen LLM.
108 | - The app also offers **coaching to improve your writing**. Its **analysis function** provides detailed, sentence-by-sentence feedback to enhance your communication.
109 | - It **measures the understandability of your text** on a scale from -10 (very complex) to +10 (very easy to understand).
110 | - The **One-Click feature sends your text to eight LLMs simultaneously**, delivering eight drafts in a formatted Word document within seconds, ready for download.
111 |
112 | In English «Einfache Sprache» is roughly equivalent to [«Plain English](https://www.plainlanguage.gov/about/definitions/), while «Leichte Sprache» has similarities to [«Easy English»](https://centreforinclusivedesign.org.au/wp-content/uploads/2020/04/Easy-English-vs-Plain-English_accessible.pdf).
113 |
114 | > [!Important]
115 | > At the risk of stating the obvious: By using the app **you send data to a third-party provider** ([OpenAI](https://platform.openai.com/docs/overview), [Anthropic](https://www.anthropic.com/api), [Google](https://cloud.google.com/docs/generative-ai) and [Mistral AI](https://docs.mistral.ai/) in case of the current state of the app). **Therefore strictly only use non-sensitive data.** Again, stating the obvious: **LLMs make errors.** They regularly hallucinate, make things up, and get things wrong. They often do so in subtle, non-obvious ways, that may be hard to detect. This app is **meant to be used as an assistive system**. It **only yields a draft, that you always must check.**
116 |
117 | **At the time of writing many users in our administration have extensively used the app with thousands of texts over more than a year. The results are very promising.** With the prototype app, our experts have saved time, improved their output, and made public communication more inclusive.
118 |
119 | > [!Note]
120 | > This **app is optimized for Swiss German** («Swiss High German», not dialect). Some rules in the prompts steer the models toward this. Also the app is **setup to use the Swiss `ss` rather than the German `ß`** The understandability index assumes the Swiss `ss` for the common word scoring and we replace `ß` with `ss` in the results.
121 |
122 | ### What does it cost?
123 |
124 | **Usage is inexpensive**. You only pay OpenAI & Co. for the tokens that you use. E.g. for the translation of 100 separate [«Normseiten»](https://de.wikipedia.org/wiki/Normseite) (standard pages of 250 German words each) to Einfache Sprache or Leichte Sprache you pay depending on the model token cost - so roughly between 0.5 CHF for GPT-4.1 mini and around 5 CHF for Claude Sonnet 3.7 (as of April 2025). The hardware requirements to run the app are modest too. As mentioned above a small VM for a couple of Francs per month will suffice.
125 |
126 | ### Our language guidelines
127 |
128 | You can find the current rules that are being prompted in `utils_prompts.py`. Have a look and change these according to your needs and organizational communication guidelines.
129 |
130 | We derived the current rules in the prompts mainly from these of our language guidelines:
131 |
132 | - [General language guidelines zh.ch](https://www.zh.ch/de/webangebote-entwickeln-und-gestalten/inhalt/inhalte-gestalten/informationen-bereitstellen/umgang-mit-sprache.html)
133 | - [Language guidelines Leichte Sprache](https://www.zh.ch/de/webangebote-entwickeln-und-gestalten/inhalt/barrierefreiheit/regeln-fuer-leichte-sprache.html)
134 | - [Guidelines Strassenverkehrsamt](https://www.zh.ch/content/dam/zhweb/bilder-dokumente/themen/politik-staat/teilhabe/erfolgsbeispiele-teilhabe/Sprachleitfaden_Strassenverkehrsamt_Maerz_2022.pdf)
135 |
136 | ### A couple of findings
137 |
138 | - **Large Language Models (LLMs) already have an understanding of Einfache Sprache, Leichte Sprache, and CEFR levels** ([A1, A2, B1, etc.](https://www.goethe.de/de/spr/kur/stu.html)) from their pretraining. It's impressive how well they can translate text by simply being asked to rewrite it according to these terms or levels. We have also successfully created test data by asking models to e.g. describe a situation at each of the six CEFR levels (A1 to C2).
139 | - **LLMs produce varied rewrites, which is beneficial**. By offering multiple model options, users receive a range of suggestions, helping them achieve a good result. It's often effective to use the One-Click mode, which consolidates results from all models.
140 | - **Measuring text understandability is really helpful**. Early in our project, we realized the need for a quantitative metric to evaluate our outputs, such as comparing different prompts, models, and preprocessing steps. We developed and index for this purpose that we call the «Zürcher Verständlichkeits-Index» or «ZIX» 😉. We created the ZIX using a dataset of complex legal and administrative texts, as well as many samples of Einfache and Leichte Sprache. We trained a classification model to differentiate between complex and simple texts. The ZIX as a metric has been very useful to us in practice. We have published the code and the Python package [here](https://github.com/machinelearningZH/zix_understandability-index).
141 | - Finally, **validating your results with your target audience is crucial**, especially for Leichte Sprache, which requires expert and user validation to be effective.
142 |
143 | ### How does the understandability score work?
144 |
145 | - The score takes into account sentence lengths, the [readability metric RIX](https://hlasse.github.io/TextDescriptives/readability.html), the occurrence of common words and overlap with the standard CEFR vocabularies A1, A2 and B1.
146 | - At the moment the score does **not** take into account other language properties that are essential for e.g. [Einfache Sprache](https://de.wikipedia.org/wiki/Einfache_Sprache) (B1 or easier, similar to «Plain English») or [Leichte Sprache](https://de.wikipedia.org/wiki/Leichte_Sprache) (A2, A1, similar to «Easy English») like use of passive voice, subjunctives, negations, etc.
147 |
148 | We have published the ZIX understandability index as a pip installable package. You can find it [here](https://github.com/machinelearningZH/zix_understandability-index).
149 |
150 | > [!Note]
151 | > The index is slightly adjusted to Swiss German. Specifically we use `ss` instead of `ß` in our vocabulary lists. In practice this should not make a big difference. For High German text that actually contains `ß` the index will likely underestimate the understandability slightly with a difference of around 0.1.
152 |
153 | ### What does the score mean?
154 |
155 | - **Negative scores indicate difficult texts in the range of B2 to C2**. These texts will likely be **very hard to understand for many people** (this is classic «Behördendeutsch» or legal text territory...).
156 | - **Positive scores indicate a language level of B1 or easier**.
157 |
158 | 
159 |
160 | ### Outlook
161 |
162 | These are a couple of areas that we are actively working on:
163 |
164 | - **Conduct more quantitative tests**: We aim to quantitatively evaluate LLM responses for completeness and accuracy. One approach we are testing is using LLMs as judges to assess these responses.
165 | - **Enhance our understandability index**: We plan to improve word scoring by detecting issues like passive voice, subjunctives and other linguistic properties that are currently missed.
166 | - **Establish standard vocabularies for administrative terms**: Consistent output for terms and names is crucial for our clients. We want to create a system that allows clients to manage these vocabularies themselves.
167 | - **Experiment with open-weight models on-premise**: To process sensitive data, we are exploring lightweight models fine-tuned with German data that can be used on-premise.
168 |
169 | ## Project team
170 |
171 | This project is a collaborative effort of these people of the cantonal administration of Zurich:
172 |
173 | - **Simone Luchetta, Roger Zedi** - [Team Informationszugang & Dialog, Staatskanzlei](https://www.zh.ch/de/staatskanzlei/digitale-verwaltung/team.html)
174 | - **Emek Sahin, Peter Hotz** - [Team Kommunikation & Entwicklung, Strassenverkehrsamt](https://www.zh.ch/de/sicherheitsdirektion/strassenverkehrsamt.html)
175 | - **Roger Meier** - [Generalsekretariat, Direktion der Justiz und des Inneren](https://www.zh.ch/de/direktion-der-justiz-und-des-innern/generalsekretariat.html#2092000119)
176 | - **Matthias Mazenauer** - [Co-Leiter, Statistisches Amt](https://www.zh.ch/de/direktion-der-justiz-und-des-innern/statistisches-amt/amtsleitung.html)
177 | - **Marisol Keller, Céline Colombo** - [Koordinationsstelle Teilhabe, Statistisches Amt](https://www.zh.ch/de/politik-staat/teilhabe.html)
178 | - **Patrick Arnecke, Chantal Amrhein, Dominik Frefel** - [Team Data, Statistisches Amt](https://www.zh.ch/de/direktion-der-justiz-und-des-innern/statistisches-amt/data.html)
179 |
180 | A special thanks goes to **[Government Councillor Jacqueline Fehr](https://www.zh.ch/en/direktion-der-justiz-und-des-innern/regierungsraetin-jacqueline-fehr.html)**, who came up with the idea and initiated and supported the project.
181 |
182 | ## Feedback and contributing
183 |
184 | We are interested to hear from you. Please share your feedback and let us know how you use the app in your institution. You can [write an email](mailto:datashop@statistik.zh.ch) or share your ideas by opening an issue or a pull requests.
185 |
186 | Please note that we use [Ruff](https://docs.astral.sh/ruff/) for linting and code formatting with default settings.
187 |
188 | ## Miscellaneous
189 |
190 | - The wonderful people at [LIIP](https://www.liip.ch/en) refactored the understandability index into a separate API ([see this repo here](https://github.com/chregu/simply-understandability-score)). They also made it available [as a webservice](https://u15y.gpt.liip.ch/). How cool is that?! 🚀 Big shoutout to [Christian Stocker](https://www.linkedin.com/in/chregu/) for doing this!
191 | - Also a big shout out to [Florian Georg](https://www.linkedin.com/in/fgeorg/) of Microsoft Switzerland for his great help to make the app work with [Azure AI](https://azure.microsoft.com/en-us/solutions/ai). Thanks!
192 |
--------------------------------------------------------------------------------
/_imgs/aoai_create_deployment.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/machinelearningZH/simply-simplify-language/35d6f9ae7fe8223a32501f27706c3cabd6b27630/_imgs/aoai_create_deployment.png
--------------------------------------------------------------------------------
/_imgs/aoai_create_deployment2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/machinelearningZH/simply-simplify-language/35d6f9ae7fe8223a32501f27706c3cabd6b27630/_imgs/aoai_create_deployment2.png
--------------------------------------------------------------------------------
/_imgs/aoai_create_service.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/machinelearningZH/simply-simplify-language/35d6f9ae7fe8223a32501f27706c3cabd6b27630/_imgs/aoai_create_service.png
--------------------------------------------------------------------------------
/_imgs/aoai_endpoint_and_key.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/machinelearningZH/simply-simplify-language/35d6f9ae7fe8223a32501f27706c3cabd6b27630/_imgs/aoai_endpoint_and_key.png
--------------------------------------------------------------------------------
/_imgs/aoai_gear_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/machinelearningZH/simply-simplify-language/35d6f9ae7fe8223a32501f27706c3cabd6b27630/_imgs/aoai_gear_icon.png
--------------------------------------------------------------------------------
/_imgs/aoai_goto_studio.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/machinelearningZH/simply-simplify-language/35d6f9ae7fe8223a32501f27706c3cabd6b27630/_imgs/aoai_goto_studio.png
--------------------------------------------------------------------------------
/_imgs/aoai_select_service.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/machinelearningZH/simply-simplify-language/35d6f9ae7fe8223a32501f27706c3cabd6b27630/_imgs/aoai_select_service.png
--------------------------------------------------------------------------------
/_imgs/aoai_select_service2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/machinelearningZH/simply-simplify-language/35d6f9ae7fe8223a32501f27706c3cabd6b27630/_imgs/aoai_select_service2.png
--------------------------------------------------------------------------------
/_imgs/app_ui.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/machinelearningZH/simply-simplify-language/35d6f9ae7fe8223a32501f27706c3cabd6b27630/_imgs/app_ui.png
--------------------------------------------------------------------------------
/_imgs/zix_scores.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/machinelearningZH/simply-simplify-language/35d6f9ae7fe8223a32501f27706c3cabd6b27630/_imgs/zix_scores.jpg
--------------------------------------------------------------------------------
/_streamlit_app/data/cefr_vocab.parq:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/machinelearningZH/simply-simplify-language/35d6f9ae7fe8223a32501f27706c3cabd6b27630/_streamlit_app/data/cefr_vocab.parq
--------------------------------------------------------------------------------
/_streamlit_app/data/ridge_regressor.pkl:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/machinelearningZH/simply-simplify-language/35d6f9ae7fe8223a32501f27706c3cabd6b27630/_streamlit_app/data/ridge_regressor.pkl
--------------------------------------------------------------------------------
/_streamlit_app/data/standard_scaler.pkl:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/machinelearningZH/simply-simplify-language/35d6f9ae7fe8223a32501f27706c3cabd6b27630/_streamlit_app/data/standard_scaler.pkl
--------------------------------------------------------------------------------
/_streamlit_app/data/word_scores_final_0728.parq:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/machinelearningZH/simply-simplify-language/35d6f9ae7fe8223a32501f27706c3cabd6b27630/_streamlit_app/data/word_scores_final_0728.parq
--------------------------------------------------------------------------------
/_streamlit_app/sprache-vereinfachen.py:
--------------------------------------------------------------------------------
1 | # ---------------------------------------------------------------
2 | # Imports
3 |
4 | import streamlit as st
5 |
6 | st.set_page_config(layout="wide")
7 |
8 | import os
9 | import re
10 | from datetime import datetime
11 | import time
12 | import base64
13 | from docx import Document
14 | from docx.shared import Pt, Inches
15 | import io
16 | from concurrent.futures import ThreadPoolExecutor
17 | from dotenv import load_dotenv
18 |
19 | import logging
20 |
21 | logging.basicConfig(
22 | filename="app.log",
23 | datefmt="%d-%b-%y %H:%M:%S",
24 | level=logging.WARNING,
25 | )
26 |
27 | import numpy as np
28 |
29 | from openai import OpenAI
30 | from anthropic import Anthropic
31 | from mistralai import Mistral
32 |
33 | from google import genai
34 | from google.genai import types
35 |
36 | from utils_understandability import get_zix, get_cefr
37 |
38 | from utils_sample_texts import (
39 | SAMPLE_TEXT_01,
40 | )
41 |
42 | from utils_prompts import (
43 | SYSTEM_MESSAGE_ES,
44 | SYSTEM_MESSAGE_LS,
45 | RULES_ES,
46 | RULES_LS,
47 | REWRITE_COMPLETE,
48 | REWRITE_CONDENSED,
49 | CLAUDE_TEMPLATE_ES,
50 | CLAUDE_TEMPLATE_LS,
51 | CLAUDE_TEMPLATE_ANALYSIS_ES,
52 | CLAUDE_TEMPLATE_ANALYSIS_LS,
53 | OPENAI_TEMPLATE_ES,
54 | OPENAI_TEMPLATE_LS,
55 | OPENAI_TEMPLATE_ANALYSIS_ES,
56 | OPENAI_TEMPLATE_ANALYSIS_LS,
57 | )
58 |
59 | CLAUDE_TEMPLATES = [
60 | CLAUDE_TEMPLATE_ES,
61 | CLAUDE_TEMPLATE_LS,
62 | CLAUDE_TEMPLATE_ANALYSIS_ES,
63 | CLAUDE_TEMPLATE_ANALYSIS_LS,
64 | ]
65 |
66 | OPENAI_TEMPLATES = [
67 | OPENAI_TEMPLATE_ES,
68 | OPENAI_TEMPLATE_LS,
69 | OPENAI_TEMPLATE_ANALYSIS_ES,
70 | OPENAI_TEMPLATE_ANALYSIS_LS,
71 | ]
72 |
73 | # ---------------------------------------------------------------
74 | # Constants
75 |
76 | load_dotenv()
77 |
78 | API_KEYS = {
79 | "OPENAI": os.getenv("OPENAI_API_KEY"),
80 | "ANTHROPIC": os.getenv("ANTHROPIC_API_KEY"),
81 | "MISTRAL": os.getenv("MISTRAL_API_KEY"),
82 | "GOOGLE": os.getenv("GOOGLE_API_KEY"),
83 | }
84 |
85 |
86 | MODEL_IDS = {
87 | "Mistral large": "mistral-large-latest",
88 | "Claude Sonnet 3.5": "claude-3-5-sonnet-latest",
89 | "Claude Sonnet 3.7": "claude-3-7-sonnet-latest",
90 | "GPT-4o": "gpt-4o",
91 | "GPT-4.1": "gpt-4.1",
92 | "GPT-4.1 mini": "gpt-4.1-mini",
93 | "Gemini 2.5 Flash": "gemini-2.5-flash-preview-05-20",
94 | "Gemini 2.5 Pro": "gemini-2.5-pro-preview-06-05",
95 | }
96 |
97 | # From our testing we derive a sensible temperature of 0.5 as a good trade-off between creativity and coherence. Adjust this to your needs.
98 | TEMPERATURE = 0.5
99 | MAX_TOKENS = 8192
100 |
101 | # Height of the text areas for input and output.
102 | TEXT_AREA_HEIGHT = 600
103 |
104 | # Maximum number of characters for the input text.
105 | # This is way below the context window sizes of the models.
106 | # Adjust to your needs. However, we found that users can work and validate better when we nudge to work with shorter texts.
107 | MAX_CHARS_INPUT = 10_000
108 |
109 |
110 | USER_WARNING = """⚠️ Achtung: Diese App ist ein Prototyp. Nutze die App :red[**nur für öffentliche, nicht sensible Daten**]. Die App liefert lediglich einen Textentwurf. Überprüfe das Ergebnis immer und passe es an, wenn nötig. Die aktuelle App-Version ist v0.8 Die letzte Aktualisierung war am 07.06.2025."""
111 |
112 |
113 | # Constants for the formatting of the Word document that can be downloaded.
114 | FONT_WORDDOC = "Arial"
115 | FONT_SIZE_HEADING = 12
116 | FONT_SIZE_PARAGRAPH = 9
117 | FONT_SIZE_FOOTER = 7
118 | DEFAULT_OUTPUT_FILENAME = "Ergebnis.docx"
119 | ANALYSIS_FILENAME = "Analyse.docx"
120 |
121 | # Limits for the understandability score to determine if the text is easy, medium or hard to understand.
122 | LIMIT_HARD = 0
123 | LIMIT_MEDIUM = -2
124 |
125 | DATETIME_FORMAT = "%Y-%m-%d %H:%M:%S"
126 |
127 | # ---------------------------------------------------------------
128 | # Functions
129 |
130 |
131 | @st.cache_resource
132 | def get_project_info():
133 | """Get markdown for project information that is shown in the expander section at the top of the app."""
134 | with open("utils_expander.md") as f:
135 | return f.read()
136 |
137 |
138 | @st.cache_resource
139 | def create_project_info(project_info):
140 | """Create expander for project info. Add the image in the middle of the content."""
141 | with st.expander("Detaillierte Informationen zum Projekt"):
142 | project_info = project_info.split("### Image ###")
143 | st.markdown(project_info[0], unsafe_allow_html=True)
144 | st.image("zix_scores.jpg", use_container_width=True)
145 | st.markdown(project_info[1], unsafe_allow_html=True)
146 |
147 |
148 | def create_prompt(text, prompt_es, prompt_ls, analysis_es, analysis_ls, analysis):
149 | """Create prompt and system message according the app settings."""
150 | if analysis:
151 | final_prompt = (
152 | analysis_ls.format(rules=RULES_LS, prompt=text)
153 | if leichte_sprache
154 | else analysis_es.format(rules=RULES_ES, prompt=text)
155 | )
156 | system = SYSTEM_MESSAGE_LS if leichte_sprache else SYSTEM_MESSAGE_ES
157 | else:
158 | if leichte_sprache:
159 | completeness = REWRITE_CONDENSED if condense_text else REWRITE_COMPLETE
160 | final_prompt = prompt_ls.format(
161 | rules=RULES_LS, completeness=completeness, prompt=text
162 | )
163 | system = SYSTEM_MESSAGE_LS
164 | else:
165 | final_prompt = prompt_es.format(
166 | rules=RULES_ES, completeness=REWRITE_COMPLETE, prompt=text
167 | )
168 | system = SYSTEM_MESSAGE_ES
169 | return final_prompt, system
170 |
171 |
172 | def get_result_from_response(response):
173 | """Extract text between tags from response."""
174 | tag = "leichtesprache" if leichte_sprache else "einfachesprache"
175 | result = re.findall(rf"<{tag}>(.*?){tag}>", response, re.DOTALL)
176 | return "\n".join(result).strip()
177 |
178 |
179 | def strip_markdown(text):
180 | """Strip markdown from text."""
181 | # Remove markdown headers.
182 | text = re.sub(r"#+\s", "", text)
183 | # Remove markdown italic and bold.
184 | text = re.sub(r"\*\*|\*|__|_", "", text)
185 | return text
186 |
187 |
188 | @st.cache_resource
189 | def get_anthropic_client():
190 | return Anthropic(api_key=API_KEYS["ANTHROPIC"])
191 |
192 |
193 | @st.cache_resource
194 | def get_openai_client():
195 | return OpenAI(api_key=API_KEYS["OPENAI"])
196 |
197 |
198 | @st.cache_resource
199 | def get_google_client():
200 | return genai.Client(api_key=API_KEYS["GOOGLE"])
201 |
202 |
203 | @st.cache_resource
204 | def get_mistral_client():
205 | return Mistral(api_key=API_KEYS["MISTRAL"])
206 |
207 |
208 | def invoke_anthropic_model(
209 | text,
210 | model_id=MODEL_IDS["Claude Sonnet 3.5"],
211 | analysis=False,
212 | ):
213 | """Invoke Anthropic model."""
214 | final_prompt, system = create_prompt(text, *CLAUDE_TEMPLATES, analysis)
215 | try:
216 | message = anthropic_client.messages.create(
217 | model=model_id,
218 | temperature=TEMPERATURE,
219 | max_tokens=MAX_TOKENS,
220 | system=system,
221 | messages=[
222 | {
223 | "role": "user",
224 | "content": final_prompt,
225 | }
226 | ],
227 | )
228 | message = message.content[0].text.strip()
229 | message = get_result_from_response(message)
230 | message = strip_markdown(message)
231 | return True, message
232 | except Exception as e:
233 | print(f"Error: {e}")
234 | return False, e
235 |
236 |
237 | def invoke_openai_model(
238 | text,
239 | model_id=MODEL_IDS["GPT-4.1 mini"],
240 | analysis=False,
241 | ):
242 | """Invoke OpenAI model."""
243 | final_prompt, system = create_prompt(text, *OPENAI_TEMPLATES, analysis)
244 | try:
245 | message = openai_client.chat.completions.create(
246 | model=model_id,
247 | temperature=TEMPERATURE,
248 | max_tokens=MAX_TOKENS,
249 | messages=[
250 | {"role": "system", "content": system},
251 | {"role": "user", "content": final_prompt},
252 | ],
253 | )
254 | message = message.choices[0].message.content.strip()
255 | message = get_result_from_response(message)
256 | message = strip_markdown(message)
257 | return True, message
258 | except Exception as e:
259 | print(f"Error: {e}")
260 | return False, e
261 |
262 |
263 | def invoke_mistral_model(
264 | text, model_id=MODEL_IDS["Mistral large"], temperature=TEMPERATURE, analysis=False
265 | ):
266 | """Invoke Mistral model."""
267 | # Our Claude templates seem to work fine for Mistral as well.
268 | final_prompt, system = create_prompt(text, *CLAUDE_TEMPLATES, analysis)
269 | messages = [
270 | {"role": "system", "content": system},
271 | {"role": "user", "content": final_prompt},
272 | ]
273 | try:
274 | message = mistral_client.chat.complete(
275 | model=model_id,
276 | messages=messages,
277 | temperature=TEMPERATURE,
278 | )
279 | message = message.choices[0].message.content.strip()
280 | message = get_result_from_response(message)
281 | message = strip_markdown(message)
282 | return True, message
283 | except Exception as e:
284 | print(f"Error: {e}")
285 | return False, e
286 |
287 |
288 | def invoke_google_model(
289 | text,
290 | model_id=MODEL_IDS["Gemini 2.5 Flash"],
291 | analysis=False,
292 | ):
293 | """Invoke Google model."""
294 | final_prompt, system = create_prompt(text, *OPENAI_TEMPLATES, analysis)
295 | try:
296 | message = google_client.models.generate_content(
297 | model=model_id,
298 | contents=final_prompt,
299 | config=types.GenerateContentConfig(
300 | system_instruction=system,
301 | max_output_tokens=MAX_TOKENS,
302 | temperature=TEMPERATURE,
303 | thinking_config=types.ThinkingConfig(thinking_budget=128),
304 | ),
305 | )
306 |
307 | message = message.text.strip()
308 | message = get_result_from_response(message)
309 | message = strip_markdown(message)
310 | return True, message
311 | except Exception as e:
312 | print(f"Error: {e}")
313 | return False, e
314 |
315 |
316 | def enter_sample_text():
317 | """Enter sample text into the text input in the left column."""
318 | st.session_state.key_textinput = SAMPLE_TEXT_01
319 |
320 |
321 | def get_one_click_results():
322 | with ThreadPoolExecutor(max_workers=9) as executor:
323 | futures = {
324 | "Mistral large": executor.submit(
325 | invoke_mistral_model,
326 | st.session_state.key_textinput,
327 | MODEL_IDS["Mistral large"],
328 | ),
329 | "GPT-4o": executor.submit(
330 | invoke_openai_model,
331 | st.session_state.key_textinput,
332 | MODEL_IDS["GPT-4o"],
333 | ),
334 | "GPT-4.1": executor.submit(
335 | invoke_openai_model,
336 | st.session_state.key_textinput,
337 | MODEL_IDS["GPT-4.1"],
338 | ),
339 | "GPT-4.1 mini": executor.submit(
340 | invoke_openai_model,
341 | st.session_state.key_textinput,
342 | MODEL_IDS["GPT-4.1 mini"],
343 | ),
344 | "Claude Sonnet 3.5": executor.submit(
345 | invoke_anthropic_model,
346 | st.session_state.key_textinput,
347 | MODEL_IDS["Claude Sonnet 3.5"],
348 | ),
349 | "Claude Sonnet 3.7": executor.submit(
350 | invoke_anthropic_model,
351 | st.session_state.key_textinput,
352 | MODEL_IDS["Claude Sonnet 3.7"],
353 | ),
354 | "Gemini 2.5 Flash": executor.submit(
355 | invoke_google_model,
356 | st.session_state.key_textinput,
357 | MODEL_IDS["Gemini 2.5 Flash"],
358 | ),
359 | "Gemini 2.5 Pro": executor.submit(
360 | invoke_google_model,
361 | st.session_state.key_textinput,
362 | MODEL_IDS["Gemini 2.5 Pro"],
363 | ),
364 | }
365 |
366 | responses = {name: future.result() for name, future in futures.items()}
367 | response_texts = []
368 |
369 | # We add 0 to the rounded ZIX score to avoid -0.
370 | # https://stackoverflow.com/a/11010791/7117003
371 | for name, (success, response) in responses.items():
372 | if success:
373 | zix = get_zix(response)
374 | zix = int(np.round(zix, 0) + 0)
375 | cefr = get_cefr(zix)
376 | response_texts.append(
377 | f"\n----- Ergebnis von {name} (Verständlichkeit: {zix}, Niveau etwa {cefr}) -----\n\n{response}"
378 | )
379 |
380 | if not response_texts:
381 | return False, "Es ist ein Fehler aufgetreten."
382 | return True, "\n\n\n".join(response_texts)
383 |
384 |
385 | def create_download_link(text_input, response, analysis=False):
386 | """Create a downloadable Word document and download link of the results."""
387 | document = Document()
388 |
389 | h1 = document.add_heading("Ausgangstext")
390 | p1 = document.add_paragraph("\n" + text_input)
391 |
392 | if analysis:
393 | h2 = document.add_heading(f"Analyse von Sprachmodell {model_choice}")
394 | elif do_one_click:
395 | h2 = document.add_heading(f"Vereinfachte Texte von Sprachmodellen")
396 | else:
397 | h2 = document.add_heading("Vereinfachter Text von Sprachmodell")
398 |
399 | p2 = document.add_paragraph(response)
400 |
401 | timestamp = datetime.now().strftime(DATETIME_FORMAT)
402 | models_used = model_choice
403 | if do_one_click:
404 | models_used = ", ".join([model for model in MODEL_IDS.keys()])
405 | footer = document.sections[0].footer
406 | footer.paragraphs[
407 | 0
408 | ].text = f"Erstellt am {timestamp} mit der Prototyp-App «Einfache Sprache», Statistisches Amt, Kanton Zürich.\nSprachmodell(e): {models_used}\nVerarbeitungszeit: {time_processed:.1f} Sekunden"
409 |
410 | # Set font for all paragraphs.
411 | for paragraph in document.paragraphs:
412 | for run in paragraph.runs:
413 | run.font.name = FONT_WORDDOC
414 |
415 | # Set font size for all headings.
416 | for paragraph in [h1, h2]:
417 | for run in paragraph.runs:
418 | run.font.size = Pt(FONT_SIZE_HEADING)
419 |
420 | # Set font size for all paragraphs.
421 | for paragraph in [p1, p2]:
422 | for run in paragraph.runs:
423 | run.font.size = Pt(FONT_SIZE_PARAGRAPH)
424 |
425 | # Set font and font size for footer.
426 | for run in footer.paragraphs[0].runs:
427 | run.font.name = "Arial"
428 | run.font.size = Pt(FONT_SIZE_FOOTER)
429 |
430 | section = document.sections[0]
431 | section.page_width = Inches(8.27) # Width of A4 paper in inches
432 | section.page_height = Inches(11.69) # Height of A4 paper in inches
433 |
434 | io_stream = io.BytesIO()
435 | document.save(io_stream)
436 |
437 | # # A download button unfortunately resets the app. So we use a link instead.
438 | # https://github.com/streamlit/streamlit/issues/4382#issuecomment-1223924851
439 | # https://discuss.streamlit.io/t/creating-a-pdf-file-generator/7613?u=volodymyr_holomb
440 |
441 | b64 = base64.b64encode(io_stream.getvalue())
442 | file_name = DEFAULT_OUTPUT_FILENAME
443 |
444 | if do_one_click:
445 | caption = "Vereinfachte Texte herunterladen"
446 | else:
447 | caption = "Vereinfachten Text herunterladen"
448 |
449 | if analysis:
450 | file_name = ANALYSIS_FILENAME
451 | caption = "Analyse herunterladen"
452 | download_url = f'{caption}'
453 | st.markdown(download_url, unsafe_allow_html=True)
454 |
455 |
456 | def clean_log(text):
457 | """Remove linebreaks and tabs from log messages
458 | that otherwise would yield problems when parsing the logs."""
459 | return text.replace("\n", " ").replace("\t", " ")
460 |
461 |
462 | def log_event(
463 | text,
464 | response,
465 | do_analysis,
466 | do_simplification,
467 | do_one_click,
468 | leichte_sprache,
469 | model_choice,
470 | time_processed,
471 | success,
472 | ):
473 | """Log event."""
474 | log_string = f"{datetime.now().strftime(DATETIME_FORMAT)}"
475 | log_string += f"\t{clean_log(text)}"
476 | log_string += f"\t{clean_log(response)}"
477 | log_string += f"\t{do_analysis}"
478 | log_string += f"\t{do_simplification}"
479 | log_string += f"\t{do_one_click}"
480 | log_string += f"\t{leichte_sprache}"
481 | log_string += f"\t{model_choice}"
482 | log_string += f"\t{time_processed:.3f}"
483 | log_string += f"\t{success}"
484 |
485 | logging.warning(log_string)
486 |
487 |
488 | # ---------------------------------------------------------------
489 | # Main
490 |
491 | anthropic_client = get_anthropic_client()
492 | openai_client = get_openai_client()
493 | google_client = get_google_client()
494 | mistral_client = get_mistral_client()
495 |
496 | project_info = get_project_info()
497 |
498 |
499 | # Persist text input across sessions in session state.
500 | # Otherwise, the text input sometimes gets lost when the user clicks on a button.
501 | if "key_textinput" not in st.session_state:
502 | st.session_state.key_textinput = ""
503 |
504 | st.markdown("## 🙋♀️ Sprache einfach vereinfachen")
505 | create_project_info(project_info)
506 | st.caption(USER_WARNING, unsafe_allow_html=True)
507 | st.markdown("---")
508 |
509 | # Set up first row with all buttons and settings.
510 | button_cols = st.columns([1, 1, 1, 2])
511 | with button_cols[0]:
512 | st.button(
513 | "Beispiel einfügen",
514 | on_click=enter_sample_text,
515 | use_container_width=True,
516 | type="secondary",
517 | help="Fügt einen Beispieltext ein.",
518 | )
519 | do_analysis = st.button(
520 | "Analysieren",
521 | use_container_width=True,
522 | help="Analysiert deinen Ausgangstext Satz für Satz.",
523 | )
524 | with button_cols[1]:
525 | do_simplification = st.button(
526 | "Vereinfachen",
527 | use_container_width=True,
528 | help="Vereinfacht deinen Ausgangstext.",
529 | )
530 | do_one_click = st.button(
531 | "🚀 One-Klick",
532 | use_container_width=True,
533 | help="Schickt deinen Ausgangstext gleichzeitig an alle Modelle.",
534 | )
535 | with button_cols[2]:
536 | leichte_sprache = st.toggle(
537 | "Leichte Sprache",
538 | value=False,
539 | help="**Schalter aktiviert**: «Leichte Sprache». **Schalter nicht aktiviert**: «Einfache Sprache».",
540 | )
541 | if leichte_sprache:
542 | condense_text = st.toggle(
543 | "Text verdichten",
544 | value=True,
545 | help="**Schalter aktiviert**: Modell konzentriert sich auf essentielle Informationen und versucht, Unwichtiges wegzulassen. **Schalter nicht aktiviert**: Modell versucht, alle Informationen zu übernehmen.",
546 | )
547 | with button_cols[3]:
548 | model_choice = st.radio(
549 | label="Sprachmodell",
550 | options=([model_name for model_name in MODEL_IDS.keys()]),
551 | index=1,
552 | horizontal=True,
553 | help="Alle Modelle liefern je nach Ausgangstext meist gute bis sehr gute Ergebnisse und sind alle einen Versuch wert. Claude Sonnet und GPT-4.1 und Google Gemini 2.5 Pro liefern sehr gute Qualität. GPT-4.1 mini ist sehr schnell. Mehr Details siehe Infobox oben auf der Seite.",
554 | )
555 |
556 | # Instantiate empty containers for the text areas.
557 | cols = st.columns([2, 2, 1])
558 |
559 | with cols[0]:
560 | source_text = st.container()
561 | with cols[1]:
562 | placeholder_result = st.empty()
563 | with cols[2]:
564 | placeholder_analysis = st.empty()
565 |
566 | # Populate containers.
567 | with source_text:
568 | st.text_area(
569 | "Ausgangstext, den du vereinfachen möchtest",
570 | value=None,
571 | height=TEXT_AREA_HEIGHT,
572 | max_chars=MAX_CHARS_INPUT,
573 | key="key_textinput",
574 | )
575 | with placeholder_result:
576 | text_output = st.text_area(
577 | "Ergebnis",
578 | height=TEXT_AREA_HEIGHT,
579 | )
580 | with placeholder_analysis:
581 | text_analysis = st.metric(
582 | label="Verständlichkeit -10 bis 10",
583 | value=None,
584 | delta=None,
585 | help="Verständlichkeit auf einer Skala von -10 bis 10 Punkten (von -10 = extrem schwer verständlich bis 10 = sehr gut verständlich). Texte in Einfacher Sprache haben meist einen Wert von 0 bis 4 oder höher, Texte in Leichter Sprache 2 bis 6 oder höher.",
586 | )
587 |
588 |
589 | # Derive model_id from explicit model_choice.
590 | model_id = MODEL_IDS[model_choice]
591 |
592 | # Start processing if one of the processing buttons is clicked.
593 | if do_simplification or do_analysis or do_one_click:
594 | start_time = time.time()
595 | if st.session_state.key_textinput == "":
596 | st.error("Bitte gib einen Text ein.")
597 | st.stop()
598 |
599 | score_source = get_zix(st.session_state.key_textinput)
600 | # We add 0 to avoid negative zero.
601 | score_source_rounded = int(np.round(score_source, 0) + 0)
602 | cefr_source = get_cefr(score_source)
603 |
604 | # Analyze source text and display results.
605 | with source_text:
606 | if score_source < LIMIT_HARD:
607 | st.markdown(
608 | f"Dein Ausgangstext ist **:red[schwer verständlich]**. ({score_source_rounded} auf einer Skala von -10 bis 10). Das entspricht etwa dem **:red[Sprachniveau {cefr_source}]**."
609 | )
610 | elif score_source >= LIMIT_HARD and score_source < LIMIT_MEDIUM:
611 | st.markdown(
612 | f"Dein Ausgangstext ist **:orange[nur mässig verständlich]**. ({score_source_rounded} auf einer Skala von -10 bis 10). Das entspricht etwa dem **:orange[Sprachniveau {cefr_source}]**."
613 | )
614 | else:
615 | st.markdown(
616 | f"Dein Ausgangstext ist **:green[gut verständlich]**. ({score_source_rounded} auf einer Skala von -10 bis 10). Das entspricht etwa dem **:green[Sprachniveau {cefr_source}]**."
617 | )
618 | with placeholder_analysis.container():
619 | text_analysis = st.metric(
620 | label="Verständlichkeit von -10 bis 10",
621 | value=score_source_rounded,
622 | delta=None,
623 | help="Verständlichkeit auf einer Skala von -10 bis 10 Punkten (von -10 = extrem schwer verständlich bis 10 = sehr gut verständlich). Texte in Einfacher Sprache haben meist einen Wert von 0 bis 4 oder höher, Texte in Leichter Sprache 2 bis 6 oder höher.",
624 | )
625 |
626 | with placeholder_analysis.container():
627 | with st.spinner("Ich arbeite..."):
628 | # One-click simplification.
629 | if do_one_click:
630 | success, response = get_one_click_results()
631 | # Regular text simplification or analysis
632 | else:
633 | if model_choice in [
634 | "GPT-4.1",
635 | "GPT-4.1 mini",
636 | "GPT-4o",
637 | ]:
638 | success, response = invoke_openai_model(
639 | st.session_state.key_textinput,
640 | model_id=model_id,
641 | analysis=do_analysis,
642 | )
643 | elif model_choice in ["Mistral large"]:
644 | success, response = invoke_mistral_model(
645 | st.session_state.key_textinput,
646 | model_id=model_id,
647 | analysis=do_analysis,
648 | )
649 | elif model_choice in [
650 | "Gemini 2.5 Flash",
651 | "Gemini 2.5 Pro",
652 | ]:
653 | success, response = invoke_google_model(
654 | st.session_state.key_textinput,
655 | model_id=model_id,
656 | analysis=do_analysis,
657 | )
658 | else:
659 | success, response = invoke_anthropic_model(
660 | st.session_state.key_textinput,
661 | model_id=model_id,
662 | analysis=do_analysis,
663 | )
664 | if success is False:
665 | st.error(
666 | "Es ist ein Fehler bei der Abfrage der APIs aufgetreten. Bitte versuche es erneut. Alternativ überprüfe Code, API-Keys, Verfügbarkeit der Modelle und ggf. Internetverbindung."
667 | )
668 | time_processed = time.time() - start_time
669 | log_event(
670 | st.session_state.key_textinput,
671 | "Error from model call",
672 | do_analysis,
673 | do_simplification,
674 | do_one_click,
675 | leichte_sprache,
676 | model_choice,
677 | time_processed,
678 | success,
679 | )
680 |
681 | st.stop()
682 |
683 | # Display results in UI.
684 | text = "Dein vereinfachter Text"
685 | if do_analysis:
686 | text = "Deine Analyse"
687 | # Often the models return the German letter «ß». Replace it with the Swiss «ss».
688 | response = response.replace("ß", "ss")
689 | time_processed = time.time() - start_time
690 |
691 | with placeholder_result.container():
692 | st.text_area(
693 | text,
694 | height=TEXT_AREA_HEIGHT,
695 | value=response,
696 | )
697 | if do_simplification or do_one_click:
698 | score_target = get_zix(response)
699 | score_target_rounded = int(np.round(score_target, 0) + 0)
700 | cefr_target = get_cefr(score_target)
701 | if score_target < LIMIT_HARD:
702 | st.markdown(
703 | f"Dein vereinfachter Text ist **:red[schwer verständlich]**. ({score_target_rounded} auf einer Skala von -10 bis 10). Das entspricht etwa dem **:red[Sprachniveau {cefr_target}]**."
704 | )
705 | elif score_target >= LIMIT_HARD and score_target < LIMIT_MEDIUM:
706 | st.markdown(
707 | f"Dein vereinfachter Text ist **:orange[nur mässig verständlich]**. ({score_target_rounded} auf einer Skala von -10 bis 10). Das entspricht etwa dem **:orange[Sprachniveau {cefr_target}]**."
708 | )
709 | else:
710 | st.markdown(
711 | f"Dein vereinfachter Text ist **:green[gut verständlich]**. ({score_target_rounded} auf einer Skala von -10 bis 10). Das entspricht etwa dem **:green[Sprachniveau {cefr_target}]**."
712 | )
713 | with placeholder_analysis.container():
714 | text_analysis = st.metric(
715 | label="Verständlichkeit -10 bis 10",
716 | value=score_target_rounded,
717 | delta=int(np.round(score_target - score_source, 0)),
718 | help="Verständlichkeit auf einer Skala von -10 bis 10 (von -10 = extrem schwer verständlich bis 10 = sehr gut verständlich). Texte in Einfacher Sprache haben meist einen Wert von 0 bis 4 oder höher.",
719 | )
720 |
721 | create_download_link(st.session_state.key_textinput, response)
722 | st.caption(f"Verarbeitet in {time_processed:.1f} Sekunden.")
723 | else:
724 | with placeholder_analysis.container():
725 | text_analysis = st.metric(
726 | label="Verständlichkeit -10 bis 10",
727 | value=score_source_rounded,
728 | help="Verständlichkeit auf einer Skala von -10 bis 10 (von -10 = extrem schwer verständlich bis 10 = sehr gut verständlich). Texte in Einfacher Sprache haben meist einen Wert von 0 bis 4 oder höher.",
729 | )
730 | create_download_link(
731 | st.session_state.key_textinput, response, analysis=True
732 | )
733 | st.caption(f"Verarbeitet in {time_processed:.1f} Sekunden.")
734 |
735 | log_event(
736 | st.session_state.key_textinput,
737 | response,
738 | do_analysis,
739 | do_simplification,
740 | do_one_click,
741 | leichte_sprache,
742 | model_choice,
743 | time_processed,
744 | success,
745 | )
746 | st.stop()
747 |
--------------------------------------------------------------------------------
/_streamlit_app/sprache-vereinfachen_azure.py:
--------------------------------------------------------------------------------
1 | # ---------------------------------------------------------------
2 | # Imports
3 |
4 | import streamlit as st
5 |
6 | st.set_page_config(layout="wide")
7 |
8 | import os
9 | import re
10 | from datetime import datetime
11 | import time
12 | import base64
13 | from docx import Document
14 | from docx.shared import Pt, Inches
15 | import io
16 | from dotenv import load_dotenv
17 |
18 | import logging
19 |
20 | logging.basicConfig(
21 | filename="app.log",
22 | datefmt="%d-%b-%y %H:%M:%S",
23 | level=logging.WARNING,
24 | )
25 |
26 | import pandas as pd
27 | import numpy as np
28 | from utils_understandability import get_zix, get_cefr
29 |
30 | # For usage of the Azure OpenAI client
31 | # see: https://github.com/openai/openai-python?tab=readme-ov-file#microsoft-azure-openai.
32 | from openai import AzureOpenAI
33 |
34 | from utils_sample_texts import (
35 | SAMPLE_TEXT_01,
36 | )
37 |
38 | from utils_prompts import (
39 | SYSTEM_MESSAGE_ES,
40 | SYSTEM_MESSAGE_LS,
41 | RULES_ES,
42 | RULES_LS,
43 | REWRITE_COMPLETE,
44 | REWRITE_CONDENSED,
45 | OPENAI_TEMPLATE_ES,
46 | OPENAI_TEMPLATE_LS,
47 | OPENAI_TEMPLATE_ANALYSIS_ES,
48 | OPENAI_TEMPLATE_ANALYSIS_LS,
49 | )
50 |
51 | OPENAI_TEMPLATES = [
52 | OPENAI_TEMPLATE_ES,
53 | OPENAI_TEMPLATE_LS,
54 | OPENAI_TEMPLATE_ANALYSIS_ES,
55 | OPENAI_TEMPLATE_ANALYSIS_LS,
56 | ]
57 |
58 |
59 | # ---------------------------------------------------------------
60 | # Constants
61 |
62 | load_dotenv()
63 |
64 | AZURE_OPENAI_API_KEY = os.getenv("AZURE_OPENAI_API_KEY")
65 | AZURE_OPENAI_DEPLOYMENT = os.getenv("AZURE_OPENAI_DEPLOYMENT")
66 | AZURE_OPENAI_ENDPOINT = os.getenv("AZURE_OPENAI_ENDPOINT")
67 |
68 | # The actually used model is defined by the 'deploymentName', see docs.
69 | MODEL_CHOICE = f"Azure OpenAI ({AZURE_OPENAI_DEPLOYMENT})"
70 | MODEL_NAME = "GPT-4o"
71 |
72 | # From our testing we derive a sensible temperature of 0.5 as a good trade-off between creativity and coherence. Adjust this to your needs.
73 | TEMPERATURE = 0.5
74 |
75 | # Height of the text areas for input and output.
76 | TEXT_AREA_HEIGHT = 600
77 |
78 | # Maximum number of characters for the input text.
79 | # This is way below the context window sizes of the models.
80 | # Adjust to your needs. However, we found that users can work and validate better when we nudge to work with shorter texts.
81 | MAX_CHARS_INPUT = 10_000
82 |
83 |
84 | USER_WARNING = """⚠️ Achtung: Diese App ist ein Prototyp. Nutze die App :red[**nur für öffentliche, nicht sensible Daten**]. Die App liefert lediglich einen Textentwurf. Überprüfe das Ergebnis immer und passe es an, wenn nötig. Die aktuelle App-Version ist v0.3 Die letzte Aktualisierung war am 30.8.2024."""
85 |
86 |
87 | # Constants for the formatting of the Word document that can be downloaded.
88 | FONT_WORDDOC = "Arial"
89 | FONT_SIZE_HEADING = 12
90 | FONT_SIZE_PARAGRAPH = 9
91 | FONT_SIZE_FOOTER = 7
92 |
93 |
94 | # Limits for the understandability score to determine if the text is easy, medium or hard to understand.
95 | LIMIT_HARD = 0
96 | LIMIT_MEDIUM = -2
97 |
98 |
99 | # ---------------------------------------------------------------
100 | # Functions
101 |
102 |
103 | @st.cache_resource
104 | def get_project_info():
105 | """Get markdown for project information that is shown in the expander section at the top of the app."""
106 | with open("utils_expander_openai.md") as f:
107 | return f.read()
108 |
109 |
110 | @st.cache_resource
111 | def create_project_info(project_info):
112 | """Create expander for project info. Add the image in the middle of the content."""
113 | with st.expander("Detaillierte Informationen zum Projekt"):
114 | project_info = project_info.split("### Image ###")
115 | st.markdown(project_info[0], unsafe_allow_html=True)
116 | st.image("zix_scores.jpg", use_column_width=True)
117 | st.markdown(project_info[1], unsafe_allow_html=True)
118 |
119 |
120 | def create_prompt(text, prompt_es, prompt_ls, analysis_es, analysis_ls, analysis):
121 | """Create prompt and system message according the app settings."""
122 | if analysis:
123 | if leichte_sprache:
124 | final_prompt = analysis_ls.format(rules=RULES_LS, prompt=text)
125 | system = SYSTEM_MESSAGE_LS
126 | else:
127 | final_prompt = analysis_es.format(rules=RULES_ES, prompt=text)
128 | system = SYSTEM_MESSAGE_ES
129 | else:
130 | if leichte_sprache:
131 | if condense_text:
132 | final_prompt = prompt_ls.format(
133 | rules=RULES_LS, completeness=REWRITE_CONDENSED, prompt=text
134 | )
135 | else:
136 | final_prompt = prompt_ls.format(
137 | rules=RULES_LS, completeness=REWRITE_COMPLETE, prompt=text
138 | )
139 | system = SYSTEM_MESSAGE_LS
140 | else:
141 | final_prompt = prompt_es.format(
142 | rules=RULES_ES, completeness=REWRITE_COMPLETE, prompt=text
143 | )
144 | system = SYSTEM_MESSAGE_ES
145 | return final_prompt, system
146 |
147 |
148 | def get_result_from_response(response):
149 | """Extract text between tags from response."""
150 | if leichte_sprache:
151 | result = re.findall(
152 | r"(.*?)", response, re.DOTALL
153 | )
154 | else:
155 | result = re.findall(
156 | r"(.*?)", response, re.DOTALL
157 | )
158 | result = "\n".join(result)
159 | return result.strip()
160 |
161 |
162 | # https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource?pivots=web-portal#create-a-resource
163 | @st.cache_resource
164 | def get_azure_openai_client():
165 | return AzureOpenAI(
166 | api_key=AZURE_OPENAI_API_KEY, # This is the default, but let's make it explicit.
167 | azure_endpoint=AZURE_OPENAI_ENDPOINT,
168 | # "2023-12-01-preview" or OPENAI_API_VERSION, see https://learn.microsoft.com/azure/ai-services/openai/reference#rest-api-versioning.
169 | api_version="2024-02-01",
170 | )
171 |
172 |
173 | # Different shape than standard OpenAI API.
174 | # See: https://github.com/openai/openai-python?tab=readme-ov-file#microsoft-azure-openai
175 | # Changes compared to OpenAI library: https://learn.microsoft.com/en-us/azure/ai-services/openai/how-to/switching-endpoints
176 |
177 |
178 | def invoke_azure_openai_model(
179 | text,
180 | deployment="simply-ch-gpt35turbo16k",
181 | temperature=TEMPERATURE,
182 | max_tokens=4096,
183 | analysis=False,
184 | ):
185 | """Invoke OpenAI model."""
186 | final_prompt, system = create_prompt(text, *OPENAI_TEMPLATES, analysis)
187 | try:
188 | message = azure_openai_client.chat.completions.create(
189 | model=deployment, # Note that Azure OpenAI needs a deployment name (not the model name).
190 | temperature=temperature,
191 | max_tokens=max_tokens,
192 | messages=[
193 | {"role": "system", "content": system},
194 | {"role": "user", "content": final_prompt},
195 | ],
196 | )
197 | message = message.choices[0].message.content.strip()
198 |
199 | return True, get_result_from_response(message)
200 | except Exception as e:
201 | print(f"Error: {e}")
202 | return False, e
203 |
204 |
205 | def enter_sample_text():
206 | """Enter sample text into the text input in the left column."""
207 | st.session_state.key_textinput = SAMPLE_TEXT_01
208 |
209 |
210 | def create_download_link(text_input, response, analysis=False):
211 | """Create a downloadable Word document and download link of the results."""
212 | document = Document()
213 |
214 | h1 = document.add_heading("Ausgangstext")
215 | p1 = document.add_paragraph("\n" + text_input)
216 |
217 | if analysis:
218 | h2 = document.add_heading(f"Analyse von Sprachmodell {MODEL_NAME} von OpenAI")
219 | else:
220 | h2 = document.add_heading(
221 | f"Vereinfachter Text von Sprachmodell {MODEL_NAME} von OpenAI"
222 | )
223 |
224 | p2 = document.add_paragraph(response)
225 |
226 | timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
227 | footer = document.sections[0].footer
228 | footer.paragraphs[
229 | 0
230 | ].text = f"Erstellt am {timestamp} mit der Prototyp-App «Einfache Sprache», Statistisches Amt, Kanton Zürich.\nSprachmodell: {MODEL_NAME}\nVerarbeitungszeit: {time_processed:.1f} Sekunden"
231 |
232 | # Set font for all paragraphs.
233 | for paragraph in document.paragraphs:
234 | for run in paragraph.runs:
235 | run.font.name = FONT_WORDDOC
236 |
237 | # Set font size for all headings.
238 | for paragraph in [h1, h2]:
239 | for run in paragraph.runs:
240 | run.font.size = Pt(FONT_SIZE_HEADING)
241 |
242 | # Set font size for all paragraphs.
243 | for paragraph in [p1, p2]:
244 | for run in paragraph.runs:
245 | run.font.size = Pt(FONT_SIZE_PARAGRAPH)
246 |
247 | # Set font and font size for footer.
248 | for run in footer.paragraphs[0].runs:
249 | run.font.name = "Arial"
250 | run.font.size = Pt(FONT_SIZE_FOOTER)
251 |
252 | section = document.sections[0]
253 | section.page_width = Inches(8.27) # Width of A4 paper in inches
254 | section.page_height = Inches(11.69) # Height of A4 paper in inches
255 |
256 | io_stream = io.BytesIO()
257 | document.save(io_stream)
258 |
259 | # # A download button unfortunately resets the app. So we use a link instead.
260 | # https://github.com/streamlit/streamlit/issues/4382#issuecomment-1223924851
261 | # https://discuss.streamlit.io/t/creating-a-pdf-file-generator/7613?u=volodymyr_holomb
262 |
263 | b64 = base64.b64encode(io_stream.getvalue())
264 | file_name = "Ergebnis.docx"
265 |
266 | caption = "Vereinfachten Text herunterladen"
267 |
268 | if analysis:
269 | file_name = "Analyse.docx"
270 | caption = "Analyse herunterladen"
271 | download_url = f'{caption}'
272 | st.markdown(download_url, unsafe_allow_html=True)
273 |
274 |
275 | def clean_log(text):
276 | """Remove linebreaks and tabs from log messages that otherwise would yield problems when parsing the logs."""
277 | text = text.replace("\n", " ")
278 | text = text.replace("\t", " ")
279 | return text
280 |
281 |
282 | def log_event(
283 | text,
284 | response,
285 | do_analysis,
286 | do_simplification,
287 | leichte_sprache,
288 | time_processed,
289 | success,
290 | ):
291 | """Log event."""
292 | log_string = f'{datetime.now().strftime("%Y-%m-%d %H:%M:%S")}'
293 | log_string += f"\t{clean_log(text)}"
294 | log_string += f"\t{clean_log(response)}"
295 | log_string += f"\t{do_analysis}"
296 | log_string += f"\t{do_simplification}"
297 | log_string += f"\t{leichte_sprache}"
298 | log_string += f"\t{time_processed:.3f}"
299 | log_string += f"\t{success}"
300 |
301 | logging.warning(log_string)
302 |
303 |
304 | # ---------------------------------------------------------------
305 | # Main
306 |
307 | azure_openai_client = get_azure_openai_client()
308 | project_info = get_project_info()
309 |
310 | # Persist text input across sessions in session state.
311 | # Otherwise, the text input sometimes gets lost when the user clicks on a button.
312 | if "key_textinput" not in st.session_state:
313 | st.session_state.key_textinput = ""
314 |
315 | st.markdown("## 🙋♀️ Sprache einfach vereinfachen (Version GPT-4o only)")
316 | create_project_info(project_info)
317 | st.caption(USER_WARNING, unsafe_allow_html=True)
318 | st.markdown("---")
319 |
320 | # Set up first row with all buttons and settings.
321 | button_cols = st.columns([1, 1, 1, 2])
322 | with button_cols[0]:
323 | st.button(
324 | "Beispiel einfügen",
325 | on_click=enter_sample_text,
326 | use_container_width=True,
327 | type="secondary",
328 | help="Fügt einen Beispieltext ein.",
329 | )
330 | with button_cols[1]:
331 | do_analysis = st.button(
332 | "Text analysieren",
333 | use_container_width=True,
334 | help="Analysiert deinen Ausgangstext Satz für Satz.",
335 | )
336 | with button_cols[2]:
337 | do_simplification = st.button(
338 | "Text vereinfachen",
339 | use_container_width=True,
340 | help="Vereinfacht deinen Ausgangstext.",
341 | )
342 | with button_cols[3]:
343 | leichte_sprache = st.toggle(
344 | "Leichte Sprache",
345 | value=False,
346 | help="**Schalter aktiviert**: «Leichte Sprache». **Schalter nicht aktiviert**: «Einfache Sprache».",
347 | )
348 | if leichte_sprache:
349 | condense_text = st.toggle(
350 | "Text verdichten",
351 | value=True,
352 | help="**Schalter aktiviert**: Modell konzentriert sich auf essentielle Informationen und versucht, Unwichtiges wegzulassen. **Schalter nicht aktiviert**: Modell versucht, alle Informationen zu übernehmen.",
353 | )
354 |
355 |
356 | # Instantiate empty containers for the text areas.
357 | cols = st.columns([2, 2, 1])
358 |
359 | with cols[0]:
360 | source_text = st.container()
361 | with cols[1]:
362 | placeholder_result = st.empty()
363 | with cols[2]:
364 | placeholder_analysis = st.empty()
365 |
366 | # Populate containers.
367 | with source_text:
368 | st.text_area(
369 | "Ausgangstext, den du vereinfachen möchtest",
370 | value=None,
371 | height=TEXT_AREA_HEIGHT,
372 | max_chars=MAX_CHARS_INPUT,
373 | key="key_textinput",
374 | )
375 | with placeholder_result:
376 | text_output = st.text_area(
377 | "Ergebnis",
378 | height=TEXT_AREA_HEIGHT,
379 | key="key_textoutput",
380 | )
381 | with placeholder_analysis:
382 | text_analysis = st.metric(
383 | label="Verständlichkeit -10 bis 10",
384 | value=None,
385 | delta=None,
386 | help="Texte in Einfacher Sprache haben meist einen Wert von 0 bis 4 oder höher, Texte in Leichter Sprache 2 bis 6 oder höher",
387 | )
388 |
389 |
390 | # Start processing if one of the processing buttons is clicked.
391 | if do_simplification or do_analysis:
392 | start_time = time.time()
393 | if st.session_state.key_textinput == "":
394 | st.error("Bitte gib einen Text ein.")
395 | st.stop()
396 |
397 | score_source = get_zix(st.session_state.key_textinput)
398 | # We add 0 to avoid negative zero.
399 | score_source_rounded = int(np.round(score_source, 0) + 0)
400 | cefr_source = get_cefr(score_source)
401 |
402 | # Analyze source text and display results.
403 | with source_text:
404 | if score_source < LIMIT_HARD:
405 | st.markdown(
406 | f"Dein Ausgangstext ist **:red[schwer verständlich]**. ({score_source_rounded} auf einer Skala von -10 bis 10). Das entspricht etwa dem **:red[Sprachniveau {cefr_source}]**."
407 | )
408 | elif score_source >= LIMIT_HARD and score_source < LIMIT_MEDIUM:
409 | st.markdown(
410 | f"Dein Ausgangstext ist **:orange[nur mässig verständlich]**. ({score_source_rounded} auf einer Skala von -10 bis 10). Das entspricht etwa dem **:orange[Sprachniveau {cefr_source}]**."
411 | )
412 | else:
413 | st.markdown(
414 | f"Dein Ausgangstext ist **:green[gut verständlich]**. ({score_source_rounded} auf einer Skala von -10 bis 10). Das entspricht etwa dem **:green[Sprachniveau {cefr_source}]**."
415 | )
416 | with placeholder_analysis.container():
417 | text_analysis = st.metric(
418 | label="Verständlichkeit von -10 bis 10",
419 | value=score_source_rounded,
420 | delta=None,
421 | help="Verständlichkeit auf einer Skala von -10 bis 10 Punkten (von -10 = extrem schwer verständlich bis 10 = sehr gut verständlich). Texte in Einfacher Sprache haben meist einen Wert von 0 bis 4 oder höher, Texte in Leichter Sprache 2 bis 6 oder höher.",
422 | )
423 |
424 | with placeholder_analysis.container():
425 | with st.spinner("Ich arbeite..."):
426 | # Regular text simplification or analysis.
427 | success, response = invoke_azure_openai_model(
428 | st.session_state.key_textinput,
429 | deployment=MODEL_CHOICE,
430 | analysis=do_analysis,
431 | )
432 |
433 | if success is False:
434 | st.error(
435 | "Es ist ein Fehler bei der Abfrage der APIs aufgetreten. Bitte versuche es erneut. Alternativ überprüfe Code, API-Keys, Verfügbarkeit der Modelle und ggf. Internetverbindung."
436 | )
437 | time_processed = time.time() - start_time
438 | log_event(
439 | text_input,
440 | "Error from model call",
441 | do_analysis,
442 | do_simplification,
443 | leichte_sprache,
444 | time_processed,
445 | success,
446 | )
447 |
448 | st.stop()
449 |
450 | # Display results in UI.
451 | text = "Dein vereinfachter Text"
452 | if do_analysis:
453 | text = "Deine Analyse"
454 | # Often the models return the German letter «ß». Replace it with the Swiss «ss».
455 | response = response.replace("ß", "ss")
456 | time_processed = time.time() - start_time
457 |
458 | with placeholder_result.container():
459 | st.text_area(
460 | text,
461 | height=TEXT_AREA_HEIGHT,
462 | value=response,
463 | )
464 | if do_simplification:
465 | score_target = get_zix(response)
466 | score_target_rounded = int(np.round(score_target, 0) + 0)
467 | cefr_target = get_cefr(score_target)
468 | if score_target < LIMIT_HARD:
469 | st.markdown(
470 | f"Dein vereinfachter Text ist **:red[schwer verständlich]**. ({score_target_rounded} auf einer Skala von -10 bis 10). Das entspricht etwa dem **:red[Sprachniveau {cefr_target}]**."
471 | )
472 | elif score_target >= LIMIT_HARD and score_target < LIMIT_MEDIUM:
473 | st.markdown(
474 | f"Dein vereinfachter Text ist **:orange[nur mässig verständlich]**. ({score_target_rounded} auf einer Skala von -10 bis 10). Das entspricht etwa dem **:orange[Sprachniveau {cefr_target}]**."
475 | )
476 | else:
477 | st.markdown(
478 | f"Dein vereinfachter Text ist **:green[gut verständlich]**. ({score_target_rounded} auf einer Skala von -10 bis 10). Das entspricht etwa dem **:green[Sprachniveau {cefr_target}]**."
479 | )
480 | with placeholder_analysis.container():
481 | text_analysis = st.metric(
482 | label="Verständlichkeit -10 bis 10",
483 | value=score_target_rounded,
484 | delta=int(np.round(score_target - score_source, 0)),
485 | help="Verständlichkeit auf einer Skala von -10 bis 10 (von -10 = extrem schwer verständlich bis 10 = sehr gut verständlich). Texte in Einfacher Sprache haben meist einen Wert von 0 bis 4 oder höher.",
486 | )
487 |
488 | create_download_link(st.session_state.key_textinput, response)
489 | st.caption(f"Verarbeitet in {time_processed:.1f} Sekunden.")
490 | else:
491 | with placeholder_analysis.container():
492 | text_analysis = st.metric(
493 | label="Verständlichkeit -10 bis 10",
494 | value=score_source_rounded,
495 | help="Verständlichkeit auf einer Skala von -10 bis 10 (von -10 = extrem schwer verständlich bis 10 = sehr gut verständlich). Texte in Einfacher Sprache haben meist einen Wert von 0 bis 4 oder höher.",
496 | )
497 | create_download_link(st.session_state.key_textinput, response, analysis=True)
498 | st.caption(f"Verarbeitet in {time_processed:.1f} Sekunden.")
499 |
500 | log_event(
501 | st.session_state.key_textinput,
502 | response,
503 | do_analysis,
504 | do_simplification,
505 | leichte_sprache,
506 | time_processed,
507 | success,
508 | )
509 | st.stop()
510 |
--------------------------------------------------------------------------------
/_streamlit_app/sprache-vereinfachen_google.py:
--------------------------------------------------------------------------------
1 | # ---------------------------------------------------------------
2 | # Imports
3 |
4 | import streamlit as st
5 |
6 | st.set_page_config(layout="wide")
7 |
8 | import os
9 | import re
10 | from datetime import datetime
11 | import time
12 | import base64
13 | from docx import Document
14 | from docx.shared import Pt, Inches
15 | import io
16 | from dotenv import load_dotenv
17 |
18 | import logging
19 |
20 | logging.basicConfig(
21 | filename="app.log",
22 | datefmt="%d-%b-%y %H:%M:%S",
23 | level=logging.WARNING,
24 | )
25 |
26 | import numpy as np
27 | from utils_understandability import get_zix, get_cefr
28 |
29 | from google import genai
30 | from google.genai import types
31 |
32 | from utils_sample_texts import (
33 | SAMPLE_TEXT_01,
34 | )
35 |
36 |
37 | from utils_prompts import (
38 | SYSTEM_MESSAGE_ES,
39 | SYSTEM_MESSAGE_LS,
40 | RULES_ES,
41 | RULES_LS,
42 | REWRITE_COMPLETE,
43 | REWRITE_CONDENSED,
44 | OPENAI_TEMPLATE_ES,
45 | OPENAI_TEMPLATE_LS,
46 | OPENAI_TEMPLATE_ANALYSIS_ES,
47 | OPENAI_TEMPLATE_ANALYSIS_LS,
48 | )
49 |
50 |
51 | OPENAI_TEMPLATES = [
52 | OPENAI_TEMPLATE_ES,
53 | OPENAI_TEMPLATE_LS,
54 | OPENAI_TEMPLATE_ANALYSIS_ES,
55 | OPENAI_TEMPLATE_ANALYSIS_LS,
56 | ]
57 |
58 |
59 | # ---------------------------------------------------------------
60 | # Constants
61 |
62 | load_dotenv()
63 | GOOGLE_API_KEY = os.getenv("GOOGLE_API_KEY")
64 |
65 | MODEL_IDS = {
66 | "Gemini 2.5 Flash": "gemini-2.5-flash-preview-05-20",
67 | "Gemini 2.5 Pro": "gemini-2.5-pro-preview-06-05",
68 | }
69 |
70 | # From our testing we derive a sensible temperature of 0.5 as a good trade-off between creativity and coherence. Adjust this to your needs.
71 | TEMPERATURE = 0.5
72 | MAX_TOKENS = 8192
73 |
74 | # Height of the text areas for input and output.
75 | TEXT_AREA_HEIGHT = 600
76 |
77 | # Maximum number of characters for the input text.
78 | # This is way below the context window sizes of the models.
79 | # Adjust to your needs. However, we found that users can work and validate better when we nudge to work with shorter texts.
80 | MAX_CHARS_INPUT = 10_000
81 |
82 | USER_WARNING = """⚠️ Achtung: Diese App ist ein Prototyp. Nutze die App :red[**nur für öffentliche, nicht sensible Daten**]. Die App liefert lediglich einen Textentwurf. Überprüfe das Ergebnis immer und passe es an, wenn nötig. Die aktuelle App-Version ist v0.5 Die letzte Aktualisierung war am 07.06.2025."""
83 |
84 | # Constants for the formatting of the Word document that can be downloaded.
85 | FONT_WORDDOC = "Arial"
86 | FONT_SIZE_HEADING = 12
87 | FONT_SIZE_PARAGRAPH = 9
88 | FONT_SIZE_FOOTER = 7
89 | DEFAULT_OUTPUT_FILENAME = "Ergebnis.docx"
90 | ANALYSIS_FILENAME = "Analyse.docx"
91 |
92 | # Limits for the understandability score to determine if the text is easy, medium or hard to understand.
93 | LIMIT_HARD = 0
94 | LIMIT_MEDIUM = -2
95 |
96 | DATETIME_FORMAT = "%Y-%m-%d %H:%M:%S"
97 |
98 | # ---------------------------------------------------------------
99 | # Functions
100 |
101 |
102 | @st.cache_resource
103 | def get_project_info():
104 | """Get markdown for project information that is shown in the expander section at the top of the app."""
105 | with open("utils_expander_google.md") as f:
106 | return f.read()
107 |
108 |
109 | @st.cache_resource
110 | def create_project_info(project_info):
111 | """Create expander for project info. Add the image in the middle of the content."""
112 | with st.expander("Detaillierte Informationen zum Projekt"):
113 | project_info = project_info.split("### Image ###")
114 | st.markdown(project_info[0], unsafe_allow_html=True)
115 | st.image("zix_scores.jpg", use_container_width=True)
116 | st.markdown(project_info[1], unsafe_allow_html=True)
117 |
118 |
119 | def create_prompt(text, prompt_es, prompt_ls, analysis_es, analysis_ls, analysis):
120 | """Create prompt and system message according the app settings."""
121 | if analysis:
122 | if leichte_sprache:
123 | final_prompt = analysis_ls.format(rules=RULES_LS, prompt=text)
124 | system = SYSTEM_MESSAGE_LS
125 | else:
126 | final_prompt = analysis_es.format(rules=RULES_ES, prompt=text)
127 | system = SYSTEM_MESSAGE_ES
128 | else:
129 | if leichte_sprache:
130 | if condense_text:
131 | final_prompt = prompt_ls.format(
132 | rules=RULES_LS, completeness=REWRITE_CONDENSED, prompt=text
133 | )
134 | else:
135 | final_prompt = prompt_ls.format(
136 | rules=RULES_LS, completeness=REWRITE_COMPLETE, prompt=text
137 | )
138 | system = SYSTEM_MESSAGE_LS
139 | else:
140 | final_prompt = prompt_es.format(
141 | rules=RULES_ES, completeness=REWRITE_COMPLETE, prompt=text
142 | )
143 | system = SYSTEM_MESSAGE_ES
144 | return final_prompt, system
145 |
146 |
147 | def get_result_from_response(response):
148 | """Extract text between tags from response."""
149 | tag = "leichtesprache" if leichte_sprache else "einfachesprache"
150 | result = re.findall(rf"<{tag}>(.*?){tag}>", response, re.DOTALL)
151 | return "\n".join(result).strip()
152 |
153 |
154 | def strip_markdown(text):
155 | """Strip markdown from text."""
156 | # Remove markdown headers.
157 | text = re.sub(r"#+\s", "", text)
158 | # Remove markdown italic and bold.
159 | text = re.sub(r"\*\*|\*|__|_", "", text)
160 | return text
161 |
162 |
163 | @st.cache_resource
164 | def get_google_client():
165 | return genai.Client(api_key=GOOGLE_API_KEY)
166 |
167 |
168 | google_client = get_google_client()
169 |
170 |
171 | def invoke_google_model(
172 | text,
173 | model_id=MODEL_IDS["Gemini 2.5 Flash"],
174 | temperature=TEMPERATURE,
175 | analysis=False,
176 | ):
177 | """Invoke Google model."""
178 | final_prompt, system = create_prompt(text, *OPENAI_TEMPLATES, analysis)
179 |
180 | try:
181 | message = google_client.models.generate_content(
182 | model=model_id,
183 | contents=final_prompt,
184 | config=types.GenerateContentConfig(
185 | system_instruction=system,
186 | max_output_tokens=MAX_TOKENS,
187 | temperature=TEMPERATURE,
188 | thinking_config=types.ThinkingConfig(thinking_budget=128),
189 | ),
190 | )
191 | message = message.text.strip()
192 | message = get_result_from_response(message)
193 | message = strip_markdown(message)
194 | return True, message
195 | except Exception as e:
196 | print(f"Error: {e}")
197 | return False, e
198 |
199 |
200 | def enter_sample_text():
201 | """Enter sample text into the text input in the left column."""
202 | st.session_state.key_textinput = SAMPLE_TEXT_01
203 |
204 |
205 | def create_download_link(text_input, response, analysis=False):
206 | """Create a downloadable Word document and download link of the results."""
207 | document = Document()
208 |
209 | h1 = document.add_heading("Ausgangstext")
210 | p1 = document.add_paragraph("\n" + text_input)
211 |
212 | if analysis:
213 | h2 = document.add_heading(f"Analyse von Sprachmodell {model_choice} von Google")
214 | else:
215 | h2 = document.add_heading(
216 | f"Vereinfachter Text von Sprachmodell {model_choice} von Google"
217 | )
218 |
219 | p2 = document.add_paragraph(response)
220 |
221 | timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
222 | footer = document.sections[0].footer
223 | footer.paragraphs[
224 | 0
225 | ].text = f"Erstellt am {timestamp} mit der Prototyp-App «Einfache Sprache», Statistisches Amt, Kanton Zürich.\nSprachmodell: {model_choice}\nVerarbeitungszeit: {time_processed:.1f} Sekunden"
226 |
227 | # Set font for all paragraphs.
228 | for paragraph in document.paragraphs:
229 | for run in paragraph.runs:
230 | run.font.name = FONT_WORDDOC
231 |
232 | # Set font size for all headings.
233 | for paragraph in [h1, h2]:
234 | for run in paragraph.runs:
235 | run.font.size = Pt(FONT_SIZE_HEADING)
236 |
237 | # Set font size for all paragraphs.
238 | for paragraph in [p1, p2]:
239 | for run in paragraph.runs:
240 | run.font.size = Pt(FONT_SIZE_PARAGRAPH)
241 |
242 | # Set font and font size for footer.
243 | for run in footer.paragraphs[0].runs:
244 | run.font.name = "Arial"
245 | run.font.size = Pt(FONT_SIZE_FOOTER)
246 |
247 | section = document.sections[0]
248 | section.page_width = Inches(8.27) # Width of A4 paper in inches
249 | section.page_height = Inches(11.69) # Height of A4 paper in inches
250 |
251 | io_stream = io.BytesIO()
252 | document.save(io_stream)
253 |
254 | # # A download button unfortunately resets the app. So we use a link instead.
255 | # https://github.com/streamlit/streamlit/issues/4382#issuecomment-1223924851
256 | # https://discuss.streamlit.io/t/creating-a-pdf-file-generator/7613?u=volodymyr_holomb
257 |
258 | b64 = base64.b64encode(io_stream.getvalue())
259 | file_name = DEFAULT_OUTPUT_FILENAME
260 | caption = "Vereinfachten Text herunterladen"
261 |
262 | if analysis:
263 | file_name = ANALYSIS_FILENAME
264 | caption = "Analyse herunterladen"
265 | download_url = f'{caption}'
266 | st.markdown(download_url, unsafe_allow_html=True)
267 |
268 |
269 | def clean_log(text):
270 | """Remove linebreaks and tabs from log messages that otherwise would yield problems when parsing the logs."""
271 | return text.replace("\n", " ").replace("\t", " ")
272 |
273 |
274 | def log_event(
275 | text,
276 | response,
277 | do_analysis,
278 | do_simplification,
279 | leichte_sprache,
280 | time_processed,
281 | success,
282 | ):
283 | """Log event."""
284 | log_string = f"{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
285 | log_string += f"\t{clean_log(text)}"
286 | log_string += f"\t{clean_log(response)}"
287 | log_string += f"\t{do_analysis}"
288 | log_string += f"\t{do_simplification}"
289 | log_string += f"\t{leichte_sprache}"
290 | log_string += f"\t{time_processed:.3f}"
291 | log_string += f"\t{success}"
292 |
293 | logging.warning(log_string)
294 |
295 |
296 | # ---------------------------------------------------------------
297 | # Main
298 |
299 | project_info = get_project_info()
300 |
301 | # Persist text input across sessions in session state.
302 | # Otherwise, the text input sometimes gets lost when the user clicks on a button.
303 | if "key_textinput" not in st.session_state:
304 | st.session_state.key_textinput = ""
305 |
306 | st.markdown("## 🙋♀️ Sprache einfach vereinfachen (Version Google Gemini only)")
307 | create_project_info(project_info)
308 | st.caption(USER_WARNING, unsafe_allow_html=True)
309 | st.markdown("---")
310 |
311 | # Set up first row with all buttons and settings.
312 | button_cols = st.columns([1, 1, 1, 2])
313 | with button_cols[0]:
314 | st.button(
315 | "Beispiel einfügen",
316 | on_click=enter_sample_text,
317 | use_container_width=True,
318 | type="secondary",
319 | help="Fügt einen Beispieltext ein.",
320 | )
321 | do_analysis = st.button(
322 | "Analysieren",
323 | use_container_width=True,
324 | help="Analysiert deinen Ausgangstext Satz für Satz.",
325 | )
326 | with button_cols[1]:
327 | do_simplification = st.button(
328 | "Vereinfachen",
329 | use_container_width=True,
330 | help="Vereinfacht deinen Ausgangstext.",
331 | )
332 |
333 | with button_cols[2]:
334 | leichte_sprache = st.toggle(
335 | "Leichte Sprache",
336 | value=False,
337 | help="**Schalter aktiviert**: «Leichte Sprache». **Schalter nicht aktiviert**: «Einfache Sprache».",
338 | )
339 | if leichte_sprache:
340 | condense_text = st.toggle(
341 | "Text verdichten",
342 | value=True,
343 | help="**Schalter aktiviert**: Modell konzentriert sich auf essentielle Informationen und versucht, Unwichtiges wegzulassen. **Schalter nicht aktiviert**: Modell versucht, alle Informationen zu übernehmen.",
344 | )
345 |
346 | with button_cols[3]:
347 | model_choice = st.radio(
348 | label="Sprachmodell",
349 | options=([model_name for model_name in MODEL_IDS.keys()]),
350 | index=1,
351 | horizontal=True,
352 | help="Gemini Flash ist schneller und liefert sehr gute Qualität. Gemini Pro ist langsamer bei bester Qualität.",
353 | )
354 |
355 |
356 | # Instantiate empty containers for the text areas.
357 | cols = st.columns([2, 2, 1])
358 |
359 | with cols[0]:
360 | source_text = st.container()
361 | with cols[1]:
362 | placeholder_result = st.empty()
363 | with cols[2]:
364 | placeholder_analysis = st.empty()
365 |
366 | # Populate containers.
367 | with source_text:
368 | st.text_area(
369 | "Ausgangstext, den du vereinfachen möchtest",
370 | value=None,
371 | height=TEXT_AREA_HEIGHT,
372 | max_chars=MAX_CHARS_INPUT,
373 | key="key_textinput",
374 | )
375 | with placeholder_result:
376 | text_output = st.text_area(
377 | "Ergebnis",
378 | height=TEXT_AREA_HEIGHT,
379 | )
380 | with placeholder_analysis:
381 | text_analysis = st.metric(
382 | label="Verständlichkeit -10 bis 10",
383 | value=None,
384 | delta=None,
385 | help="Verständlichkeit auf einer Skala von -10 bis 10 Punkten (von -10 = extrem schwer verständlich bis 10 = sehr gut verständlich). Texte in Einfacher Sprache haben meist einen Wert von 0 bis 4 oder höher, Texte in Leichter Sprache 2 bis 6 oder höher.",
386 | )
387 |
388 | model_id = MODEL_IDS[model_choice]
389 |
390 |
391 | # Start processing if one of the processing buttons is clicked.
392 | if do_simplification or do_analysis:
393 | start_time = time.time()
394 | if st.session_state.key_textinput == "":
395 | st.error("Bitte gib einen Text ein.")
396 | st.stop()
397 |
398 | score_source = get_zix(st.session_state.key_textinput)
399 | # We add 0 to avoid negative zero.
400 | score_source_rounded = int(np.round(score_source, 0) + 0)
401 | cefr_source = get_cefr(score_source)
402 |
403 | # Analyze source text and display results.
404 | with source_text:
405 | if score_source < LIMIT_HARD:
406 | st.markdown(
407 | f"Dein Ausgangstext ist **:red[schwer verständlich]**. ({score_source_rounded} auf einer Skala von -10 bis 10). Das entspricht etwa dem **:red[Sprachniveau {cefr_source}]**."
408 | )
409 | elif score_source >= LIMIT_HARD and score_source < LIMIT_MEDIUM:
410 | st.markdown(
411 | f"Dein Ausgangstext ist **:orange[nur mässig verständlich]**. ({score_source_rounded} auf einer Skala von -10 bis 10). Das entspricht etwa dem **:orange[Sprachniveau {cefr_source}]**."
412 | )
413 | else:
414 | st.markdown(
415 | f"Dein Ausgangstext ist **:green[gut verständlich]**. ({score_source_rounded} auf einer Skala von -10 bis 10). Das entspricht etwa dem **:green[Sprachniveau {cefr_source}]**."
416 | )
417 | with placeholder_analysis.container():
418 | text_analysis = st.metric(
419 | label="Verständlichkeit von -10 bis 10",
420 | value=score_source_rounded,
421 | delta=None,
422 | help="Verständlichkeit auf einer Skala von -10 bis 10 Punkten (von -10 = extrem schwer verständlich bis 10 = sehr gut verständlich). Texte in Einfacher Sprache haben meist einen Wert von 0 bis 4 oder höher, Texte in Leichter Sprache 2 bis 6 oder höher.",
423 | )
424 |
425 | with placeholder_analysis.container():
426 | with st.spinner("Ich arbeite..."):
427 | # Regular text simplification or analysis.
428 | success, response = invoke_google_model(
429 | st.session_state.key_textinput,
430 | model_id=model_id,
431 | analysis=do_analysis,
432 | )
433 |
434 | if success is False:
435 | st.error(
436 | "Es ist ein Fehler bei der Abfrage der APIs aufgetreten. Bitte versuche es erneut. Alternativ überprüfe Code, API-Keys, Verfügbarkeit der Modelle und ggf. Internetverbindung."
437 | )
438 | time_processed = time.time() - start_time
439 | log_event(
440 | st.session_state.key_textinput,
441 | "Error from model call",
442 | do_analysis,
443 | do_simplification,
444 | leichte_sprache,
445 | time_processed,
446 | success,
447 | )
448 |
449 | st.stop()
450 |
451 | # Display results in UI.
452 | text = "Dein vereinfachter Text"
453 | if do_analysis:
454 | text = "Deine Analyse"
455 | # Often the models return the German letter «ß». Replace it with the Swiss «ss».
456 | response = response.replace("ß", "ss")
457 | time_processed = time.time() - start_time
458 |
459 | with placeholder_result.container():
460 | st.text_area(
461 | text,
462 | height=TEXT_AREA_HEIGHT,
463 | value=response,
464 | )
465 | if do_simplification:
466 | score_target = get_zix(response)
467 | score_target_rounded = int(np.round(score_target, 0) + 0)
468 | cefr_target = get_cefr(score_target)
469 | if score_target < LIMIT_HARD:
470 | st.markdown(
471 | f"Dein vereinfachter Text ist **:red[schwer verständlich]**. ({score_target_rounded} auf einer Skala von -10 bis 10). Das entspricht etwa dem **:red[Sprachniveau {cefr_target}]**."
472 | )
473 | elif score_target >= LIMIT_HARD and score_target < LIMIT_MEDIUM:
474 | st.markdown(
475 | f"Dein vereinfachter Text ist **:orange[nur mässig verständlich]**. ({score_target_rounded} auf einer Skala von -10 bis 10). Das entspricht etwa dem **:orange[Sprachniveau {cefr_target}]**."
476 | )
477 | else:
478 | st.markdown(
479 | f"Dein vereinfachter Text ist **:green[gut verständlich]**. ({score_target_rounded} auf einer Skala von -10 bis 10). Das entspricht etwa dem **:green[Sprachniveau {cefr_target}]**."
480 | )
481 | with placeholder_analysis.container():
482 | text_analysis = st.metric(
483 | label="Verständlichkeit -10 bis 10",
484 | value=score_target_rounded,
485 | delta=int(np.round(score_target - score_source, 0)),
486 | help="Verständlichkeit auf einer Skala von -10 bis 10 (von -10 = extrem schwer verständlich bis 10 = sehr gut verständlich). Texte in Einfacher Sprache haben meist einen Wert von 0 bis 4 oder höher.",
487 | )
488 |
489 | create_download_link(st.session_state.key_textinput, response)
490 | st.caption(f"Verarbeitet in {time_processed:.1f} Sekunden.")
491 | else:
492 | with placeholder_analysis.container():
493 | text_analysis = st.metric(
494 | label="Verständlichkeit -10 bis 10",
495 | value=score_source_rounded,
496 | help="Verständlichkeit auf einer Skala von -10 bis 10 (von -10 = extrem schwer verständlich bis 10 = sehr gut verständlich). Texte in Einfacher Sprache haben meist einen Wert von 0 bis 4 oder höher.",
497 | )
498 | create_download_link(
499 | st.session_state.key_textinput, response, analysis=True
500 | )
501 | st.caption(f"Verarbeitet in {time_processed:.1f} Sekunden.")
502 |
503 | log_event(
504 | st.session_state.key_textinput,
505 | response,
506 | do_analysis,
507 | do_simplification,
508 | leichte_sprache,
509 | time_processed,
510 | success,
511 | )
512 | st.stop()
513 |
--------------------------------------------------------------------------------
/_streamlit_app/sprache-vereinfachen_openai.py:
--------------------------------------------------------------------------------
1 | # ---------------------------------------------------------------
2 | # Imports
3 |
4 | import streamlit as st
5 |
6 | st.set_page_config(layout="wide")
7 |
8 | import os
9 | import re
10 | from datetime import datetime
11 | import time
12 | import base64
13 | from docx import Document
14 | from docx.shared import Pt, Inches
15 | import io
16 | from dotenv import load_dotenv
17 |
18 | import logging
19 |
20 | logging.basicConfig(
21 | filename="app.log",
22 | datefmt="%d-%b-%y %H:%M:%S",
23 | level=logging.WARNING,
24 | )
25 |
26 | import numpy as np
27 | from utils_understandability import get_zix, get_cefr
28 |
29 | from openai import OpenAI
30 |
31 | from utils_sample_texts import (
32 | SAMPLE_TEXT_01,
33 | )
34 |
35 |
36 | from utils_prompts import (
37 | SYSTEM_MESSAGE_ES,
38 | SYSTEM_MESSAGE_LS,
39 | RULES_ES,
40 | RULES_LS,
41 | REWRITE_COMPLETE,
42 | REWRITE_CONDENSED,
43 | OPENAI_TEMPLATE_ES,
44 | OPENAI_TEMPLATE_LS,
45 | OPENAI_TEMPLATE_ANALYSIS_ES,
46 | OPENAI_TEMPLATE_ANALYSIS_LS,
47 | )
48 |
49 |
50 | OPENAI_TEMPLATES = [
51 | OPENAI_TEMPLATE_ES,
52 | OPENAI_TEMPLATE_LS,
53 | OPENAI_TEMPLATE_ANALYSIS_ES,
54 | OPENAI_TEMPLATE_ANALYSIS_LS,
55 | ]
56 |
57 |
58 | # ---------------------------------------------------------------
59 | # Constants
60 |
61 | load_dotenv()
62 | OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
63 |
64 | MODEL_IDS = {
65 | "GPT-4o mini": "gpt-4o-mini",
66 | "GPT-4o": "gpt-4o",
67 | "GPT-4.1 mini": "gpt-4.1-mini",
68 | "GPT-4.1": "gpt-4.1",
69 | "o1 mini": "o1-mini",
70 | "o1": "o1",
71 | "o3 mini": "o3-mini",
72 | "o3": "o3",
73 | "o4 mini": "o4-mini",
74 | }
75 |
76 | # From our testing we derive a sensible temperature of 0.5 as a good trade-off between creativity and coherence. Adjust this to your needs.
77 | TEMPERATURE = 0.5
78 | MAX_TOKENS = 8192
79 |
80 | # Height of the text areas for input and output.
81 | TEXT_AREA_HEIGHT = 600
82 |
83 | # Maximum number of characters for the input text.
84 | # This is way below the context window sizes of the models.
85 | # Adjust to your needs. However, we found that users can work and validate better when we nudge to work with shorter texts.
86 | MAX_CHARS_INPUT = 10_000
87 |
88 |
89 | USER_WARNING = """⚠️ Achtung: Diese App ist ein Prototyp. Nutze die App :red[**nur für öffentliche, nicht sensible Daten**]. Die App liefert lediglich einen Textentwurf. Überprüfe das Ergebnis immer und passe es an, wenn nötig. Die aktuelle App-Version ist v0.7 Die letzte Aktualisierung war am 18.04.2025."""
90 |
91 |
92 | # Constants for the formatting of the Word document that can be downloaded.
93 | FONT_WORDDOC = "Arial"
94 | FONT_SIZE_HEADING = 12
95 | FONT_SIZE_PARAGRAPH = 9
96 | FONT_SIZE_FOOTER = 7
97 | DEFAULT_OUTPUT_FILENAME = "Ergebnis.docx"
98 | ANALYSIS_FILENAME = "Analyse.docx"
99 |
100 |
101 | # Limits for the understandability score to determine if the text is easy, medium or hard to understand.
102 | LIMIT_HARD = 0
103 | LIMIT_MEDIUM = -2
104 |
105 |
106 | # ---------------------------------------------------------------
107 | # Functions
108 |
109 |
110 | @st.cache_resource
111 | def get_project_info():
112 | """Get markdown for project information that is shown in the expander section at the top of the app."""
113 | with open("utils_expander_openai.md") as f:
114 | return f.read()
115 |
116 |
117 | @st.cache_resource
118 | def create_project_info(project_info):
119 | """Create expander for project info. Add the image in the middle of the content."""
120 | with st.expander("Detaillierte Informationen zum Projekt"):
121 | project_info = project_info.split("### Image ###")
122 | st.markdown(project_info[0], unsafe_allow_html=True)
123 | st.image("zix_scores.jpg", use_container_width=True)
124 | st.markdown(project_info[1], unsafe_allow_html=True)
125 |
126 |
127 | def create_prompt(text, prompt_es, prompt_ls, analysis_es, analysis_ls, analysis):
128 | """Create prompt and system message according the app settings."""
129 | if analysis:
130 | if leichte_sprache:
131 | final_prompt = analysis_ls.format(rules=RULES_LS, prompt=text)
132 | system = SYSTEM_MESSAGE_LS
133 | else:
134 | final_prompt = analysis_es.format(rules=RULES_ES, prompt=text)
135 | system = SYSTEM_MESSAGE_ES
136 | else:
137 | if leichte_sprache:
138 | if condense_text:
139 | final_prompt = prompt_ls.format(
140 | rules=RULES_LS, completeness=REWRITE_CONDENSED, prompt=text
141 | )
142 | else:
143 | final_prompt = prompt_ls.format(
144 | rules=RULES_LS, completeness=REWRITE_COMPLETE, prompt=text
145 | )
146 | system = SYSTEM_MESSAGE_LS
147 | else:
148 | final_prompt = prompt_es.format(
149 | rules=RULES_ES, completeness=REWRITE_COMPLETE, prompt=text
150 | )
151 | system = SYSTEM_MESSAGE_ES
152 | return final_prompt, system
153 |
154 |
155 | def get_result_from_response(response):
156 | """Extract text between tags from response."""
157 | tag = "leichtesprache" if leichte_sprache else "einfachesprache"
158 | result = re.findall(rf"<{tag}>(.*?){tag}>", response, re.DOTALL)
159 | return "\n".join(result).strip()
160 |
161 |
162 | def strip_markdown(text):
163 | """Strip markdown from text."""
164 | # Remove markdown headers.
165 | text = re.sub(r"#+\s", "", text)
166 | # Remove markdown italic and bold.
167 | text = re.sub(r"\*\*|\*|__|_", "", text)
168 | return text
169 |
170 |
171 | @st.cache_resource
172 | def get_openai_client():
173 | return OpenAI(api_key=OPENAI_API_KEY)
174 |
175 |
176 | def invoke_openai_model(
177 | text,
178 | model_id=MODEL_IDS["GPT-4.1 mini"],
179 | analysis=False,
180 | ):
181 | """Invoke OpenAI model."""
182 | final_prompt, system = create_prompt(text, *OPENAI_TEMPLATES, analysis)
183 | try:
184 | message = openai_client.chat.completions.create(
185 | model=model_id,
186 | temperature=TEMPERATURE,
187 | max_tokens=MAX_TOKENS,
188 | messages=[
189 | {"role": "system", "content": system},
190 | {"role": "user", "content": final_prompt},
191 | ],
192 | )
193 | message = message.choices[0].message.content.strip()
194 | message = get_result_from_response(message)
195 | message = strip_markdown(message)
196 | return True, message
197 | except Exception as e:
198 | print(f"Error: {e}")
199 | return False, e
200 |
201 |
202 | def invoke_openai_reasoning_model(
203 | text,
204 | model_id=MODEL_IDS["o1 mini"],
205 | analysis=False,
206 | ):
207 | """Invoke OpenAI model."""
208 | final_prompt, _ = create_prompt(text, *OPENAI_TEMPLATES, analysis)
209 | try:
210 | message = openai_client.chat.completions.create(
211 | model=model_id,
212 | messages=[
213 | {"role": "user", "content": final_prompt},
214 | ],
215 | )
216 | message = message.choices[0].message.content.strip()
217 | message = get_result_from_response(message)
218 | message = strip_markdown(message)
219 | return True, message
220 | except Exception as e:
221 | print(f"Error: {e}")
222 | return False, e
223 |
224 |
225 | def enter_sample_text():
226 | """Enter sample text into the text input in the left column."""
227 | st.session_state.key_textinput = SAMPLE_TEXT_01
228 |
229 |
230 | def create_download_link(text_input, response, analysis=False):
231 | """Create a downloadable Word document and download link of the results."""
232 | document = Document()
233 |
234 | h1 = document.add_heading("Ausgangstext")
235 | p1 = document.add_paragraph("\n" + text_input)
236 |
237 | if analysis:
238 | h2 = document.add_heading(f"Analyse von Sprachmodell {model_choice} von OpenAI")
239 | else:
240 | h2 = document.add_heading(
241 | f"Vereinfachter Text von Sprachmodell {model_choice} von OpenAI"
242 | )
243 |
244 | p2 = document.add_paragraph(response)
245 |
246 | timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
247 | footer = document.sections[0].footer
248 | footer.paragraphs[
249 | 0
250 | ].text = f"Erstellt am {timestamp} mit der Prototyp-App «Einfache Sprache», Statistisches Amt, Kanton Zürich.\nSprachmodell: {model_choice}\nVerarbeitungszeit: {time_processed:.1f} Sekunden"
251 |
252 | # Set font for all paragraphs.
253 | for paragraph in document.paragraphs:
254 | for run in paragraph.runs:
255 | run.font.name = FONT_WORDDOC
256 |
257 | # Set font size for all headings.
258 | for paragraph in [h1, h2]:
259 | for run in paragraph.runs:
260 | run.font.size = Pt(FONT_SIZE_HEADING)
261 |
262 | # Set font size for all paragraphs.
263 | for paragraph in [p1, p2]:
264 | for run in paragraph.runs:
265 | run.font.size = Pt(FONT_SIZE_PARAGRAPH)
266 |
267 | # Set font and font size for footer.
268 | for run in footer.paragraphs[0].runs:
269 | run.font.name = "Arial"
270 | run.font.size = Pt(FONT_SIZE_FOOTER)
271 |
272 | section = document.sections[0]
273 | section.page_width = Inches(8.27) # Width of A4 paper in inches
274 | section.page_height = Inches(11.69) # Height of A4 paper in inches
275 |
276 | io_stream = io.BytesIO()
277 | document.save(io_stream)
278 |
279 | # # A download button unfortunately resets the app. So we use a link instead.
280 | # https://github.com/streamlit/streamlit/issues/4382#issuecomment-1223924851
281 | # https://discuss.streamlit.io/t/creating-a-pdf-file-generator/7613?u=volodymyr_holomb
282 |
283 | b64 = base64.b64encode(io_stream.getvalue())
284 | file_name = DEFAULT_OUTPUT_FILENAME
285 |
286 | caption = "Vereinfachten Text herunterladen"
287 |
288 | if analysis:
289 | file_name = ANALYSIS_FILENAME
290 | caption = "Analyse herunterladen"
291 | download_url = f'{caption}'
292 | st.markdown(download_url, unsafe_allow_html=True)
293 |
294 |
295 | def clean_log(text):
296 | """Remove linebreaks and tabs from log messages that otherwise would yield problems when parsing the logs."""
297 | return text.replace("\n", " ").replace("\t", " ")
298 |
299 |
300 | def log_event(
301 | text,
302 | response,
303 | do_analysis,
304 | do_simplification,
305 | leichte_sprache,
306 | time_processed,
307 | success,
308 | ):
309 | """Log event."""
310 | log_string = f"{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
311 | log_string += f"\t{clean_log(text)}"
312 | log_string += f"\t{clean_log(response)}"
313 | log_string += f"\t{do_analysis}"
314 | log_string += f"\t{do_simplification}"
315 | log_string += f"\t{leichte_sprache}"
316 | log_string += f"\t{time_processed:.3f}"
317 | log_string += f"\t{success}"
318 |
319 | logging.warning(log_string)
320 |
321 |
322 | # ---------------------------------------------------------------
323 | # Main
324 |
325 | openai_client = get_openai_client()
326 | project_info = get_project_info()
327 |
328 | # Persist text input across sessions in session state.
329 | # Otherwise, the text input sometimes gets lost when the user clicks on a button.
330 | if "key_textinput" not in st.session_state:
331 | st.session_state.key_textinput = ""
332 |
333 | st.markdown("## 🙋♀️ Sprache einfach vereinfachen (Version «OpenAI only»)")
334 | create_project_info(project_info)
335 | st.caption(USER_WARNING, unsafe_allow_html=True)
336 | st.markdown("---")
337 |
338 | # Set up first row with all buttons and settings.
339 | button_cols = st.columns([1, 1, 1, 2])
340 | with button_cols[0]:
341 | st.button(
342 | "Beispiel einfügen",
343 | on_click=enter_sample_text,
344 | use_container_width=True,
345 | type="secondary",
346 | help="Fügt einen Beispieltext ein.",
347 | )
348 | do_analysis = st.button(
349 | "Analysieren",
350 | use_container_width=True,
351 | help="Analysiert deinen Ausgangstext Satz für Satz.",
352 | )
353 | with button_cols[1]:
354 | do_simplification = st.button(
355 | "Vereinfachen",
356 | use_container_width=True,
357 | help="Vereinfacht deinen Ausgangstext.",
358 | )
359 | with button_cols[2]:
360 | leichte_sprache = st.toggle(
361 | "Leichte Sprache",
362 | value=False,
363 | help="**Schalter aktiviert**: «Leichte Sprache». **Schalter nicht aktiviert**: «Einfache Sprache».",
364 | )
365 | if leichte_sprache:
366 | condense_text = st.toggle(
367 | "Text verdichten",
368 | value=True,
369 | help="**Schalter aktiviert**: Modell konzentriert sich auf essentielle Informationen und versucht, Unwichtiges wegzulassen. **Schalter nicht aktiviert**: Modell versucht, alle Informationen zu übernehmen.",
370 | )
371 |
372 | with button_cols[3]:
373 | model_choice = st.radio(
374 | label="Sprachmodell",
375 | options=([model_name for model_name in MODEL_IDS.keys()]),
376 | index=2,
377 | horizontal=True,
378 | )
379 |
380 |
381 | # Instantiate empty containers for the text areas.
382 | cols = st.columns([2, 2, 1])
383 |
384 | with cols[0]:
385 | source_text = st.container()
386 | with cols[1]:
387 | placeholder_result = st.empty()
388 | with cols[2]:
389 | placeholder_analysis = st.empty()
390 |
391 | # Populate containers.
392 | with source_text:
393 | st.text_area(
394 | "Ausgangstext, den du vereinfachen möchtest",
395 | value=None,
396 | height=TEXT_AREA_HEIGHT,
397 | max_chars=MAX_CHARS_INPUT,
398 | key="key_textinput",
399 | )
400 | with placeholder_result:
401 | text_output = st.text_area(
402 | "Ergebnis",
403 | height=TEXT_AREA_HEIGHT,
404 | )
405 | with placeholder_analysis:
406 | text_analysis = st.metric(
407 | label="Verständlichkeit -10 bis 10",
408 | value=None,
409 | delta=None,
410 | help="Verständlichkeit auf einer Skala von -10 bis 10 Punkten (von -10 = extrem schwer verständlich bis 10 = sehr gut verständlich). Texte in Einfacher Sprache haben meist einen Wert von 0 bis 4 oder höher, Texte in Leichter Sprache 2 bis 6 oder höher.",
411 | )
412 |
413 | # Derive model_id from explicit model_choice.
414 | model_id = MODEL_IDS[model_choice]
415 |
416 | # Start processing if one of the processing buttons is clicked.
417 | if do_simplification or do_analysis:
418 | start_time = time.time()
419 | if st.session_state.key_textinput == "":
420 | st.error("Bitte gib einen Text ein.")
421 | st.stop()
422 |
423 | score_source = get_zix(st.session_state.key_textinput)
424 | # We add 0 to avoid negative zero.
425 | score_source_rounded = int(np.round(score_source, 0) + 0)
426 | cefr_source = get_cefr(score_source)
427 |
428 | # Analyze source text and display results.
429 | with source_text:
430 | if score_source < LIMIT_HARD:
431 | st.markdown(
432 | f"Dein Ausgangstext ist **:red[schwer verständlich]**. ({score_source_rounded} auf einer Skala von -10 bis 10). Das entspricht etwa dem **:red[Sprachniveau {cefr_source}]**."
433 | )
434 | elif score_source >= LIMIT_HARD and score_source < LIMIT_MEDIUM:
435 | st.markdown(
436 | f"Dein Ausgangstext ist **:orange[nur mässig verständlich]**. ({score_source_rounded} auf einer Skala von -10 bis 10). Das entspricht etwa dem **:orange[Sprachniveau {cefr_source}]**."
437 | )
438 | else:
439 | st.markdown(
440 | f"Dein Ausgangstext ist **:green[gut verständlich]**. ({score_source_rounded} auf einer Skala von -10 bis 10). Das entspricht etwa dem **:green[Sprachniveau {cefr_source}]**."
441 | )
442 | with placeholder_analysis.container():
443 | text_analysis = st.metric(
444 | label="Verständlichkeit von -10 bis 10",
445 | value=score_source_rounded,
446 | delta=None,
447 | help="Verständlichkeit auf einer Skala von -10 bis 10 Punkten (von -10 = extrem schwer verständlich bis 10 = sehr gut verständlich). Texte in Einfacher Sprache haben meist einen Wert von 0 bis 4 oder höher, Texte in Leichter Sprache 2 bis 6 oder höher.",
448 | )
449 |
450 | with placeholder_analysis.container():
451 | with st.spinner("Ich arbeite..."):
452 | # Regular text simplification or analysis.
453 | if "GPT" in model_choice:
454 | success, response = invoke_openai_model(
455 | st.session_state.key_textinput,
456 | model_id=model_id,
457 | analysis=do_analysis,
458 | )
459 | else:
460 | success, response = invoke_openai_reasoning_model(
461 | st.session_state.key_textinput,
462 | model_id=model_id,
463 | analysis=do_analysis,
464 | )
465 |
466 | if success is False:
467 | st.error(
468 | "Es ist ein Fehler bei der Abfrage der APIs aufgetreten. Bitte versuche es erneut. Alternativ überprüfe Code, API-Keys, Verfügbarkeit der Modelle und ggf. Internetverbindung."
469 | )
470 | time_processed = time.time() - start_time
471 | log_event(
472 | st.session_state.key_textinput,
473 | "Error from model call",
474 | do_analysis,
475 | do_simplification,
476 | leichte_sprache,
477 | time_processed,
478 | success,
479 | )
480 |
481 | st.stop()
482 |
483 | # Display results in UI.
484 | text = "Dein vereinfachter Text"
485 | if do_analysis:
486 | text = "Deine Analyse"
487 | # Often the models return the German letter «ß». Replace it with the Swiss «ss».
488 | response = response.replace("ß", "ss")
489 | time_processed = time.time() - start_time
490 |
491 | with placeholder_result.container():
492 | st.text_area(
493 | text,
494 | height=TEXT_AREA_HEIGHT,
495 | value=response,
496 | )
497 | if do_simplification:
498 | score_target = get_zix(response)
499 | score_target_rounded = int(np.round(score_target, 0) + 0)
500 | cefr_target = get_cefr(score_target)
501 | if score_target < LIMIT_HARD:
502 | st.markdown(
503 | f"Dein vereinfachter Text ist **:red[schwer verständlich]**. ({score_target_rounded} auf einer Skala von -10 bis 10). Das entspricht etwa dem **:red[Sprachniveau {cefr_target}]**."
504 | )
505 | elif score_target >= LIMIT_HARD and score_target < LIMIT_MEDIUM:
506 | st.markdown(
507 | f"Dein vereinfachter Text ist **:orange[nur mässig verständlich]**. ({score_target_rounded} auf einer Skala von -10 bis 10). Das entspricht etwa dem **:orange[Sprachniveau {cefr_target}]**."
508 | )
509 | else:
510 | st.markdown(
511 | f"Dein vereinfachter Text ist **:green[gut verständlich]**. ({score_target_rounded} auf einer Skala von -10 bis 10). Das entspricht etwa dem **:green[Sprachniveau {cefr_target}]**."
512 | )
513 | with placeholder_analysis.container():
514 | text_analysis = st.metric(
515 | label="Verständlichkeit -10 bis 10",
516 | value=score_target_rounded,
517 | delta=int(np.round(score_target - score_source, 0)),
518 | help="Verständlichkeit auf einer Skala von -10 bis 10 (von -10 = extrem schwer verständlich bis 10 = sehr gut verständlich). Texte in Einfacher Sprache haben meist einen Wert von 0 bis 4 oder höher.",
519 | )
520 |
521 | create_download_link(st.session_state.key_textinput, response)
522 | st.caption(f"Verarbeitet in {time_processed:.1f} Sekunden.")
523 | else:
524 | with placeholder_analysis.container():
525 | text_analysis = st.metric(
526 | label="Verständlichkeit -10 bis 10",
527 | value=score_source_rounded,
528 | help="Verständlichkeit auf einer Skala von -10 bis 10 (von -10 = extrem schwer verständlich bis 10 = sehr gut verständlich). Texte in Einfacher Sprache haben meist einen Wert von 0 bis 4 oder höher.",
529 | )
530 | create_download_link(
531 | st.session_state.key_textinput, response, analysis=True
532 | )
533 | st.caption(f"Verarbeitet in {time_processed:.1f} Sekunden.")
534 |
535 | log_event(
536 | st.session_state.key_textinput,
537 | response,
538 | do_analysis,
539 | do_simplification,
540 | leichte_sprache,
541 | time_processed,
542 | success,
543 | )
544 | st.stop()
545 |
--------------------------------------------------------------------------------
/_streamlit_app/utils_expander.md:
--------------------------------------------------------------------------------
1 | Dieser Prototyp ist Teil eines Projekts vom Statistischen Amt, Kanton Zürich. Mit diesem Projekt möchten wir öffentliche Organisationen dabei unterstützen, ihre Kommunikation noch verständlicher aufzubereiten.
2 |
3 | ## Wichtig
4 |
5 | - **:red[Nutze die App nur für öffentliche, nicht sensible Daten.]**
6 | - **:red[Die App liefert lediglich einen Entwurf. Überprüfe das Ergebnis immer – idealerweise mit Menschen aus dem Zielpublikum – und passe es an, wenn nötig.]**
7 |
8 | ## Was macht diese App?
9 |
10 | **Diese App versucht, einen von dir eingegebenen Text in Einfache Sprache oder Leichte Sprache zu übersetzen.**
11 |
12 | Dein Text wird dazu in der App aufbereitet und an ein sogenanntes grosses Sprachmodell (LLM, Large Language Model) eines kommerziellen Anbieters geschickt. Diese Sprachmodelle sind in der Lage, Texte nach Anweisungen umzuformulieren und dabei zu vereinfachen.
13 |
14 | Du kannst die Texte nach den Regeln für Einfache Sprache oder Leichte Sprache übersetzen.
15 |
16 | **Leichte Sprache** ist eine vereinfachte Form der deutschen Sprache, die nach bestimmten Regeln gestaltet wird. Leicht Sprache hilft u.a. Menschen mit Lernschwierigkeiten oder geringen Deutschkenntnissen.
17 |
18 | **Einfache Sprache** ist eine vereinfachte Version von Alltagssprache. Diese zielt darauf, Texte generell für ein breiteres Publikum verständlicher zu machen.
19 |
20 | **Die Texte werden teils in sehr guter Qualität vereinfacht. Sie sind aber nie 100% korrekt. Die App liefert lediglich einen Entwurf. Die Texte müssen immer von dir überprüft und angepasst werden. Insbesondere bei Leichter Sprache ist [die Überprüfung der Ergebnisse durch Prüferinnen und Prüfer aus dem Zielpublikum essentiell](https://www.leichte-sprache.org/leichte-sprache/das-pruefen/).**
21 |
22 | ### Modus «Leichte Sprache»?
23 |
24 | In der Grundeinstellung übersetzt die App in Einfache Spache. Wenn du den Schalter «Leichte Sprache» klickst, weist du das Modell an, einen Entwurf in **Leichter Sprache** zu schreiben. Wenn Leichte Sprache aktiviert ist, kannst du zusätzlich wählen, ob das Modell alle Informationen übernehmen oder versuchen soll, sinnvoll zu verdichten.
25 |
26 | ### Was sind die verschiedenen Sprachmodelle?
27 |
28 | Momentan kannst du eins von 8 Sprachmodellen wählen:
29 |
30 | - **Mistral Large**: sehr gute Qualität
31 | - **Claude V3.5 Sonnet**: sehr gute Qualität
32 | - **Claude V3.7 Sonnet**: beste Qualität
33 | - **GPT-4o**: sehr gute Qualität
34 | - **GPT-4.1 mini**: sehr gute Qualität
35 | - **GPT-4.1**: beste Qualität
36 | - **Gemini 2.5 Flash**: sehr gute Qualität
37 | - **Gemini 2.5 Pro**: beste Qualität
38 |
39 | Alle Modelle analysieren und schreiben unterschiedlich und sind alle einen Versuch wert. Die Claude-Modelle werden von [Anthropic](https://www.anthropic.com/) betrieben, die GPT- und o1-Modelle von [OpenAI](https://openai.com/), die Mistral-Modelle von [Mistral](https://mistral.ai/) und die Gemini-Modelle von [Google](https://ai.google.dev/).
40 |
41 | ### Wie funktioniert die Bewertung der Verständlichkeit?
42 |
43 | Wir haben einen Algorithmus entwickelt, der die Verständlichkeit von Texten auf einer Skala von -10 bis 10 bewertet. Dieser Algorithmus basiert auf diversen Textmerkmalen: Den Wort- und Satzlängen, dem [Lesbarkeitsindex RIX](https://www.jstor.org/stable/40031755), der Häufigkeit von einfachen, verständlichen, viel genutzten Worten, sowie dem Anteil an Worten aus dem Standardvokabular A1, A2 und B1. Wir haben dies systematisch ermittelt, indem wir geschaut haben, welche Merkmale am aussagekräftigsten für Verwaltungs- und Rechtssprache und deren Vereinfachung sind.
44 |
45 | Die Bewertung kannst du so interpretieren:
46 |
47 | - **Sehr schwer verständliche Texte** wie Rechts- oder Verwaltungstexte haben meist Werte von **-10 bis -2**.
48 | - **Durchschnittlich verständliche Texte** wie Nachrichtentexte, Wikipediaartikel oder Bücher haben meist Werte von **-2 bis 0**.
49 | - **Gut verständliche Texte im Bereich Einfacher Sprache und Leichter Sprache** haben meist Werte von **0 oder grösser.**.
50 |
51 | Wir zeigen dir zusätzlich eine **grobe** Schätzung des Sprachniveaus gemäss [CEFR (Common European Framework of Reference for Languages)](https://www.coe.int/en/web/common-european-framework-reference-languages/level-descriptions) von A1 bis C2 an.
52 |
53 | ### Image ###
54 |
55 | Die Bewertung ist bei weitem nicht perfekt, aber sie ist ein guter erster Anhaltspunkt und hat sich bei unseren Praxistests bewährt.
56 |
57 | ### Feedback
58 |
59 | Wir sind für Rückmeldungen und Anregungen jeglicher Art dankbar und nehmen diese jederzeit gern [per Mail entgegen](mailto:datashop@statistik.zh.ch).
60 |
61 | ## Versionsverlauf
62 |
63 | - **v0.8** - 07.06.2025 - *Modelle aktualisiert. Bugfixes.*
64 | - **v0.7** - 18.04.2025 - *Modelle aktualisiert. GPT-4.1 und GPT-4.1 mini, neue Gemini-Modelle. Refactoring zu aktuellem Google GenAI Python SDK.*
65 | - **v0.6** - 29.03.2025 - *Modelle aktualisiert.*
66 | - **v0.5** - 22.12.2024 - *Modelle aktualisiert und ergänzt. Code vereinfacht.*
67 | - **v0.4** - 30.08.2024 - *Fehler behoben.*
68 | - **v0.3** - 18.08.2024 - *Neuen ZIX-Index integriert. Diverse Fehler behoben.*
69 | - **v0.2** - 21.06.2024 - *Update auf Claude Sonnet v3.5.*
70 | - **v0.1** - 1.06.2024 - *Erste Open Source-Version der App auf Basis des bisherigen Pilotprojekts.*
71 |
--------------------------------------------------------------------------------
/_streamlit_app/utils_expander_google.md:
--------------------------------------------------------------------------------
1 | Dieser Prototyp ist Teil eines Projekts vom Statistischen Amt, Kanton Zürich. Mit diesem Projekt möchten wir öffentliche Organisationen dabei unterstützen, ihre Kommunikation noch verständlicher aufzubereiten.
2 |
3 | ## Wichtig
4 |
5 | - **:red[Nutze die App nur für öffentliche, nicht sensible Daten.]**
6 | - **:red[Die App liefert lediglich einen Entwurf. Überprüfe das Ergebnis immer – idealerweise mit Menschen aus dem Zielpublikum – und passe es an, wenn nötig.]**
7 |
8 | ## Was macht diese App?
9 |
10 | **Diese App übersetzt einen Text in Entwürfe für Einfache Sprache oder Leichte Sprache.**
11 |
12 | Dein Text wird dazu in der App aufbereitet und an ein sogenanntes grosses Sprachmodell (LLM, Large Language Model) eines kommerziellen Anbieters geschickt. Diese Sprachmodelle sind in der Lage, Texte nach Anweisungen umzuformulieren und dabei zu vereinfachen.
13 |
14 | **Die Texte werden teils in sehr guter Qualität vereinfacht. Sie sind aber nie 100% korrekt. Die App liefert lediglich einen Entwurf. Die Texte müssen immer von dir überprüft und angepasst werden. Insbesondere bei Leichter Sprache ist [die Überprüfung der Ergebnisse durch Prüferinnen und Prüfer aus dem Zielpublikum essentiell](https://www.leichte-sprache.org/leichte-sprache/das-pruefen/).**
15 |
16 | ### Was ist der Modus «Leichte Sprache»?
17 |
18 | Mit dem Schalter «Leichte Sprache» kannst du das Modell anweisen, einen ***Entwurf*** in Leichter Sprache zu schreiben. Wenn Leichte Sprache aktiviert ist, kannst du zusätzlich wählen, ob das Modell alle Informationen übernehmen oder versuchen soll, sinnvoll zu verdichten.
19 |
20 | ### Welches Sprachmodell wird verwendet?
21 |
22 | In dieser App-Variante werden die Sprachmodelle Gemini 2.5 Flash und Gemini 2.5 Pro von [Google](https://ai.google.dev/gemini-api/docs/models) verwendet.
23 |
24 | ### Wie funktioniert die Bewertung der Verständlichkeit?
25 |
26 | Wir haben einen Algorithmus entwickelt, der die Verständlichkeit von Texten auf einer Skala von -10 bis 10 bewertet. Dieser Algorithmus basiert auf diversen Textmerkmalen: Den Wort- und Satzlängen, dem [Lesbarkeitsindex RIX](https://www.jstor.org/stable/40031755), der Häufigkeit von einfachen, verständlichen, viel genutzten Worten, sowie dem Anteil an Worten aus dem Standardvokabular A1, A2 und B1. Wir haben dies systematisch ermittelt, indem wir geschaut haben, welche Merkmale am aussagekräftigsten für Verwaltungs- und Rechtssprache und deren Vereinfachung sind.
27 |
28 | Die Bewertung kannst du so interpretieren:
29 |
30 | - **Sehr schwer verständliche Texte** wie Rechts- oder Verwaltungstexte haben meist Werte von **-10 bis -2**.
31 | - **Durchschnittlich verständliche Texte** wie Nachrichtentexte, Wikipediaartikel oder Bücher haben meist Werte von **-2 bis 0**.
32 | - **Gut verständliche Texte im Bereich Einfacher Sprache und Leichter Sprache** haben meist Werte von **0 oder grösser.**.
33 |
34 | Wir zeigen dir zusätzlich eine **grobe** Schätzung des Sprachniveaus gemäss [CEFR (Common European Framework of Reference for Languages)](https://www.coe.int/en/web/common-european-framework-reference-languages/level-descriptions) von A1 bis C2 an.
35 |
36 | ### Image ###
37 |
38 | Die Bewertung ist bei weitem nicht perfekt, aber sie ist ein guter erster Anhaltspunkt und hat sich bei unseren Praxistests bewährt.
39 |
40 | ### Feedback
41 |
42 | Wir sind für Rückmeldungen und Anregungen jeglicher Art dankbar und nehmen diese jederzeit gern [per Mail entgegen](mailto:datashop@statistik.zh.ch).
43 |
44 | ## Versionsverlauf
45 |
46 | - **v0.5** – 07.06.2025 – *Modelle aktualisiert. Bugfixes.*
47 | - **v0.4** – 18.04.2025 – *Modelle aktualisiert. Refactoring zu neuem Gemini SDK.*
48 | - **v0.3** – 29.03.2025 – *Modelle aktualisiert.*
49 | - **v0.2** – 22.12.2024 – *Modelle aktualisiert und ergänzt: Gemini Flash 2.0. Code vereinfacht.*
50 | - **v0.1** - 02.09.2024 - *App-Version für Google Gemini Modelle.*
51 |
--------------------------------------------------------------------------------
/_streamlit_app/utils_expander_openai.md:
--------------------------------------------------------------------------------
1 | Dieser Prototyp ist Teil eines Projekts vom Statistischen Amt, Kanton Zürich. Mit diesem Projekt möchten wir öffentliche Organisationen dabei unterstützen, ihre Kommunikation noch verständlicher aufzubereiten.
2 |
3 | ## Wichtig
4 |
5 | - **:red[Nutze die App nur für öffentliche, nicht sensible Daten.]**
6 | - **:red[Die App liefert lediglich einen Entwurf. Überprüfe das Ergebnis immer – idealerweise mit Menschen aus dem Zielpublikum – und passe es an, wenn nötig.]**
7 |
8 | ## Was macht diese App?
9 |
10 | **Diese App übersetzt einen Text in Entwürfe für Einfache Sprache oder Leichte Sprache.**
11 |
12 | Dein Text wird dazu in der App aufbereitet und an ein sogenanntes grosses Sprachmodell (LLM, Large Language Model) eines kommerziellen Anbieters geschickt. Diese Sprachmodelle sind in der Lage, Texte nach Anweisungen umzuformulieren und dabei zu vereinfachen.
13 |
14 | **Die Texte werden teils in sehr guter Qualität vereinfacht. Sie sind aber nie 100% korrekt. Die App liefert lediglich einen Entwurf. Die Texte müssen immer von dir überprüft und angepasst werden. Insbesondere bei Leichter Sprache ist [die Überprüfung der Ergebnisse durch Prüferinnen und Prüfer aus dem Zielpublikum essentiell](https://www.leichte-sprache.org/leichte-sprache/das-pruefen/).**
15 |
16 | ### Was ist der Modus «Leichte Sprache»?
17 |
18 | Mit dem Schalter «Leichte Sprache» kannst du das Modell anweisen, einen ***Entwurf*** in Leichter Sprache zu schreiben. Wenn Leichte Sprache aktiviert ist, kannst du zusätzlich wählen, ob das Modell alle Informationen übernehmen oder versuchen soll, sinnvoll zu verdichten.
19 |
20 | ### Welches Sprachmodell wird verwendet?
21 |
22 | In dieser App-Variante können wir die Sprachmodelle von [OpenAI](https://openai.com/) nutzen.
23 |
24 | - **GPT-4o-mini**: gute Qualität
25 | - **GPT-4o**: sehr gute Qualität
26 | - **GPT-4.1-mini**: gute Qualität
27 | - **GPT-4.1**: sehr gute Qualität
28 | - **o1 mini**: [«Reasoning-Modell»](https://openai.com/o1/)
29 | - **o1**: [«Reasoning-Modell»](https://openai.com/o1/)
30 | - **o3 mini**: [«Reasoning-Modell»](https://openai.com/index/introducing-o3-and-o4-mini/)
31 | - **o3**: [«Reasoning-Modell»](https://openai.com/index/introducing-o3-and-o4-mini/)
32 | - **o4 mini**: [«Reasoning-Modell»](https://openai.com/index/introducing-o3-and-o4-mini/)
33 |
34 | ### Wie funktioniert die Bewertung der Verständlichkeit?
35 |
36 | Wir haben einen Algorithmus entwickelt, der die Verständlichkeit von Texten auf einer Skala von -10 bis 10 bewertet. Dieser Algorithmus basiert auf diversen Textmerkmalen: Den Wort- und Satzlängen, dem [Lesbarkeitsindex RIX](https://www.jstor.org/stable/40031755), der Häufigkeit von einfachen, verständlichen, viel genutzten Worten, sowie dem Anteil an Worten aus dem Standardvokabular A1, A2 und B1. Wir haben dies systematisch ermittelt, indem wir geschaut haben, welche Merkmale am aussagekräftigsten für Verwaltungs- und Rechtssprache und deren Vereinfachung sind.
37 |
38 | Die Bewertung kannst du so interpretieren:
39 |
40 | - **Sehr schwer verständliche Texte** wie Rechts- oder Verwaltungstexte haben meist Werte von **-10 bis -2**.
41 | - **Durchschnittlich verständliche Texte** wie Nachrichtentexte, Wikipediaartikel oder Bücher haben meist Werte von **-2 bis 0**.
42 | - **Gut verständliche Texte im Bereich Einfacher Sprache und Leichter Sprache** haben meist Werte von **0 oder grösser.**.
43 |
44 | Wir zeigen dir zusätzlich eine **grobe** Schätzung des Sprachniveaus gemäss [CEFR (Common European Framework of Reference for Languages)](https://www.coe.int/en/web/common-european-framework-reference-languages/level-descriptions) von A1 bis C2 an.
45 |
46 | ### Image ###
47 |
48 | Die Bewertung ist bei weitem nicht perfekt, aber sie ist ein guter erster Anhaltspunkt und hat sich bei unseren Praxistests bewährt.
49 |
50 | ### Feedback
51 |
52 | Wir sind für Rückmeldungen und Anregungen jeglicher Art dankbar und nehmen diese jederzeit gern [per Mail entgegen](mailto:datashop@statistik.zh.ch).
53 |
54 | ## Versionsverlauf
55 |
56 | - **v0.6** – 18.04.2025 – *Modelle aktualisiert.*
57 | - **v0.5** – 22.12.2024 – *Modelle aktualisiert und ergänzt: o1 und o1 mini. Code vereinfacht.*
58 | - **v0.4** – 30.08.2024 – *Fehler behoben.*
59 | - **v0.3** – 18.08.2024 – *Neuer ZIX-Index integriert. Diverse Fehler behoben.*
60 | - **v0.2** – 14.06.2024 – *App-Variante, die nur GPT-4o verwendet.*
61 | - **v0.1** – 01.06.2024 – *Erste Open Source-Version der App auf Basis des bisherigen Pilotprojekts.*
62 |
--------------------------------------------------------------------------------
/_streamlit_app/utils_prompts.py:
--------------------------------------------------------------------------------
1 | # We derived the following prompts for «Einfache Sprache» (ES) and «Leichte Sprache» (LS) mainly from our guidelines of the administration of the Canton of Zurich. According to our testing these are good defaults and prove to be helpful for our employees. However, we strongly recommend to validate and adjust these rules to the specific needs of your organization.
2 |
3 | # References:
4 | # https://www.zh.ch/de/webangebote-entwickeln-und-gestalten/inhalt/inhalte-gestalten/informationen-bereitstellen/umgang-mit-sprache.html
5 | # https://www.zh.ch/de/webangebote-entwickeln-und-gestalten/inhalt/barrierefreiheit/regeln-fuer-leichte-sprache.html
6 | # https://www.zh.ch/content/dam/zhweb/bilder-dokumente/themen/politik-staat/teilhabe/erfolgsbeispiele-teilhabe/Sprachleitfaden_Strassenverkehrsamt_Maerz_2022.pdf
7 |
8 | # Note that Anthropic recommends to put the content first and the prompt last. This is the opposite of what usually is the prompt structure for OpenAI models.
9 | # https://docs.anthropic.com/en/docs/long-context-window-tips#document-query-placement
10 | # Note also that Claude models prefer XML tags for structuring whereas OpenAI models prefer Markdown (used here) or JSON.
11 | # We use the Claude prompt structure for Mistral with good success. Feel free to adjust the structure to your needs.
12 |
13 |
14 | SYSTEM_MESSAGE_ES = """Du bist ein hilfreicher Assistent, der Texte in Einfache Sprache, Sprachniveau B1 bis A2, umschreibt. Sei immer wahrheitsgemäß und objektiv. Schreibe nur das, was du sicher aus dem Text des Benutzers weisst. Arbeite die Texte immer vollständig durch und kürze nicht. Mache keine Annahmen. Schreibe einfach und klar und immer in deutscher Sprache. Gib dein Ergebnis innerhalb von Tags aus."""
15 |
16 |
17 | SYSTEM_MESSAGE_LS = """Du bist ein hilfreicher Assistent, der Texte in Leichte Sprache, Sprachniveau A2 bis A1, umschreibt. Sei immer wahrheitsgemäß und objektiv. Schreibe nur das, was du sicher aus dem Text des Benutzers weisst. Arbeite die Texte immer vollständig durch und kürze nicht. Mache keine Annahmen. Schreibe einfach und klar und immer in deutscher Sprache. Gib dein Ergebnis innerhalb von Tags aus."""
18 |
19 |
20 | RULES_ES = """
21 | - Schreibe kurze Sätze mit höchstens 12 Wörtern.
22 | - Beschränke dich auf eine Aussage, einen Gedanken pro Satz.
23 | - Verwende aktive Sprache anstelle von Passiv.
24 | - Formuliere grundsätzlich positiv und bejahend.
25 | - Strukturiere den Text übersichtlich mit kurzen Absätzen.
26 | - Verwende einfache, kurze, häufig gebräuchliche Wörter.
27 | - Wenn zwei Wörter dasselbe bedeuten, verwende das kürzere und einfachere Wort.
28 | - Vermeide Füllwörter und unnötige Wiederholungen.
29 | - Erkläre Fachbegriffe und Fremdwörter.
30 | - Schreibe immer einfach, direkt und klar. Vermeide komplizierte Konstruktionen und veraltete Begriffe. Vermeide «Behördendeutsch».
31 | - Benenne Gleiches immer gleich. Verwende für denselben Begriff, Gegenstand oder Sachverhalt immer dieselbe Bezeichnung. Wiederholungen von Begriffen sind in Texten in Einfacher Sprache normal.
32 | - Vermeide Substantivierungen. Verwende stattdessen Verben und Adjektive.
33 | - Vermeide Adjektive und Adverbien, wenn sie nicht unbedingt notwendig sind.
34 | - Wenn du vier oder mehr Wörter zusammensetzt, setzt du Bindestriche. Beispiel: «Motorfahrzeug-Ausweispflicht».
35 | - Achte auf die sprachliche Gleichbehandlung von Mann und Frau. Verwende immer beide Geschlechter oder schreibe geschlechtsneutral.
36 | - Vermeide Abkürzungen grundsätzlich. Schreibe stattdessen die Wörter aus. Z.B. «10 Millionen» statt «10 Mio.», «200 Kilometer pro Stunde» statt «200 km/h», «zum Beispiel» statt «z.B.», «30 Prozent» statt «30 %», «2 Meter» statt «2 m», «das heisst» statt «d.h.».
37 | - Vermeide das stumme «e» am Wortende, wenn es nicht unbedingt notwendig ist. Zum Beispiel: «des Fahrzeugs» statt «des Fahrzeuges».
38 | - Verwende immer französische Anführungszeichen (« ») anstelle von deutschen Anführungszeichen („ “).
39 | - Gliedere Telefonnummern mit vier Leerzeichen. Z.B. 044 123 45 67. Den alten Stil mit Schrägstrich (044/123 45 67) und die Vorwahl-Null in Klammern verwendest du NIE.
40 | - Formatiere Datumsangaben immer so: 1. Januar 2022, 15. Februar 2022.
41 | - Jahreszahlen schreibst du immer vierstellig aus: 2022, 2025-2030.
42 | - Formatiere Zeitangaben immer «Stunden Punkt Minuten Uhr». Verwende keinen Doppelpunkt, um Stunden von Minuten zu trennen. Ergänze immer .00 bei vollen Stunden. Beispiele: 9.25 Uhr (NICHT 9:30), 10.30 Uhr (NICHT 10:00), 14.00 Uhr (NICHT 14 Uhr), 15.45 Uhr, 18.00 Uhr, 20.15 Uhr, 22.30 Uhr.
43 | - Zahlen bis 12 schreibst du aus. Ab 13 verwendest du Ziffern.
44 | - Fristen, Geldbeträge und physikalische Grössen schreibst du immer in Ziffern.
45 | - Zahlen, die zusammengehören, schreibst du immer in Ziffern. Beispiel: 5-10, 20 oder 30.
46 | - Grosse Zahlen ab 5 Stellen gliederst du in Dreiergruppen mit Leerzeichen. Beispiel: 1 000 000.
47 | - Achtung: Identifikationszahlen übernimmst du 1:1. Beispiel: Stammnummer 123.456.789, AHV-Nummer 756.1234.5678.90, Konto 01-100101-9.
48 | - Verwende das Komma, dass das deutsche Dezimalzeichen ist. Überflüssige Nullen nach dem Komma schreibst du nicht. Beispiel: 5,5 Millionen, 3,75 Prozent, 1,5 Kilometer, 2,25 Stunden.
49 | - Vor Franken-Rappen-Beträgen schreibst du immer «CHF». Nur nach ganzen Franken-Beträgen darfst du «Franken» schreiben. Bei Franken- Rappen-Beträgen setzt du einen Punkt als Dezimalzeichen. Anstatt des Null-Rappen-Strichs verwendest du «.00» oder lässt die Dezimalstellen weg. Z.B. 20 Franken, CHF 20, CHF 2.00, CHF 12.50, aber CHF 45,2 Millionen, EUR 14,90.
50 | - Die Anrede mit «Sie» schreibst du immer gross. Beispiel: «Sie haben».
51 | """.strip()
52 |
53 |
54 | RULES_LS = """
55 | - Schreibe wichtiges zuerst: Beginne den Text mit den wichtigsten Informationen, so dass diese sofort klar werden.
56 | - Verwende einfache, kurze, häufig gebräuchliche Wörter.
57 | - Löse zusammengesetzte Wörter auf und formuliere sie neu.
58 | - Wenn es wichtige Gründe gibt, ein zusammengesetztes Wort nicht aufzulösen, trenne das zusammengesetzte Wort mit einem Bindestrich. Beginne dann jedes Wort mit einem Grossbuchstaben. Beispiele: «Auto-Service», «Gegen-Argument», «Kinder-Betreuung», «Volks-Abstimmung».
59 | - Vermeide Fremdwörter. Wähle stattdessen einfache, allgemein bekannte Wörter. Erkläre Fremdwörter, wenn sie unvermeidbar sind.
60 | - Vermeide Fachbegriffe. Wähle stattdessen einfache, allgemein bekannte Wörter. Erkläre Fachbegriffe, wenn sie unvermeidbar sind.
61 | - Vermeide bildliche Sprache. Verwende keine Metaphern oder Redewendungen. Schreibe stattdessen klar und direkt.
62 | - Schreibe kurze Sätze mit optimal 8 und höchstens 12 Wörtern.
63 | - Du darfst Relativsätze mit «der», «die», «das» verwenden.
64 | - Löse Nebensätze nach folgenden Regeln auf:
65 | - Kausalsätze (weil, da): Löse Kausalsätze als zwei Hauptsätze mit «deshalb» auf.
66 | - Konditionalsätze (wenn, falls): Löse Konditionalsätze als zwei Hauptsätze mit «vielleicht» auf.
67 | - Finalsätze (damit, dass): Löse Finalsätze als zwei Hauptsätze mit «deshalb» auf.
68 | - Konzessivsätze (obwohl, obgleich, wenngleich, auch wenn): Löse Konzessivsätze als zwei Hauptsätze mit «trotzdem» auf.
69 | - Temporalsätze (als, während, bevor, nachdem, sobald, seit): Löse Temporalsätze als einzelne chronologische Sätze auf. Wenn es passt, verknüpfe diese mit «dann».
70 | - Adversativsätze (aber, doch, jedoch, allerdings, sondern, allein): Löse Adversativsätze als zwei Hauptsätze mit «aber» auf.
71 | - Modalsätze (indem, dadurch dass): Löse Modalsätze als zwei Hauptsätze auf. Z.B. Alltagssprache: Er lernt besser, indem er regelmässig übt. Leichte Sprache: Er lernt besser. Er übt regelmässig.
72 | - Konsekutivsätze (so dass, sodass): Löse Konsekutivsätze als zwei Hauptsätze auf. Z.B. Alltagssprache: Er ist krank, sodass er nicht arbeiten konnte. Leichte Sprache: Er ist krank. Er konnte nicht arbeiten.
73 | - Relativsätze mit «welcher», «welche», «welches»: Löse solche Relativsätze als zwei Hauptsätze auf. Z.B. Alltagssprache: Das Auto, welches rot ist, steht vor dem Haus. Leichte Sprache: Das Auto ist rot. Das Auto steht vor dem Haus.
74 | - Ob-Sätze: Schreibe Ob-Sätze als zwei Hauptsätze. Z.B. Alltagssprache: Er fragt, ob es schönes Wetter wird. Leichte Sprache: Er fragt: Wird es schönes Wetter?
75 | - Verwende aktive Sprache anstelle von Passiv.
76 | - Benutze den Genitiv nur in einfachen Fällen. Verwende stattdessen die Präposition "von" und den Dativ.
77 | - Vermeide das stumme «e» am Wortende, wenn es nicht unbedingt notwendig ist. Zum Beispiel: «des Fahrzeugs» statt «des Fahrzeuges».
78 | - Bevorzuge die Vorgegenwart (Perfekt). Vermeide die Vergangenheitsform (Präteritum), wenn möglich. Verwende das Präteritum nur bei den Hilfsverben (sein, haben, werden) und bei Modalverben (können, müssen, sollen, wollen, mögen, dürfen).
79 | - Benenne Gleiches immer gleich. Verwende für denselben Begriff, Gegenstand oder Sachverhalt immer dieselbe Bezeichnung. Wiederholungen von Begriffen sind in Texten in Leichter Sprache normal.
80 | - Vermeide Pronomen. Verwende Pronomen nur, wenn der Bezug ganz klar ist. Sonst wiederhole das Nomen.
81 | - Formuliere grundsätzlich positiv und bejahend. Vermeide Verneinungen ganz.
82 | - Verwende IMMER die Satzstellung Subjekt-Prädikat-Objekt.
83 | - Vermeide Substantivierungen. Verwende stattdessen Verben und Adjektive.
84 | - Achte auf die sprachliche Gleichbehandlung von Mann und Frau. Verwende immer beide Geschlechter oder schreibe geschlechtsneutral.
85 | - Vermeide Abkürzungen grundsätzlich. Schreibe stattdessen die Wörter aus. Z.B. «10 Millionen» statt «10 Mio.», «200 Kilometer pro Stunde» statt «200 km/h», «zum Beispiel» statt «z.B.», «30 Prozent» statt «30 %», «2 Meter» statt «2 m», «das heisst» statt «d.h.». Je nach Kontext kann es aber sinnvoll sein, eine Abkürzung einzuführen. Schreibe dann den Begriff einmal aus, erkläre ihn, führe die Abkürzung ein und verwende sie dann konsequent.
86 | - Schreibe die Abkürzungen «usw.», «z.B.», «etc.» aus. Also zum Beispiel «und so weiter», «zum Beispiel», «etcetera».
87 | - Formatiere Zeitangaben immer «Stunden Punkt Minuten Uhr». Verwende keinen Doppelpunkt, um Stunden von Minuten zu trennen. Ergänze immer .00 bei vollen Stunden. Beispiele: 9.25 Uhr (NICHT 9:30), 10.30 Uhr (NICHT 10:00), 14.00 Uhr (NICHT 14 Uhr), 15.45 Uhr, 18.00 Uhr, 20.15 Uhr, 22.30 Uhr.
88 | - Formatiere Datumsangaben immer so: 1. Januar 2022, 15. Februar 2022.
89 | - Jahreszahlen schreibst du immer vierstellig aus: 2022, 2025-2030.
90 | - Verwende immer französische Anführungszeichen (« ») anstelle von deutschen Anführungszeichen („ “).
91 | - Gliedere Telefonnummern mit vier Leerzeichen. Z.B. 044 123 45 67. Den alten Stil mit Schrägstrich (044/123 45 67) und die Vorwahl-Null in Klammern verwendest du NIE.
92 | - Zahlen bis 12 schreibst du aus. Ab 13 verwendest du Ziffern.
93 | - Fristen, Geldbeträge und physikalische Grössen schreibst du immer in Ziffern.
94 | - Zahlen, die zusammengehören, schreibst du immer in Ziffern. Beispiel: 5-10, 20 oder 30.
95 | - Grosse Zahlen ab 5 Stellen gliederst du in Dreiergruppen mit Leerzeichen. Beispiel: 1 000 000.
96 | - Achtung: Identifikationszahlen übernimmst du 1:1. Beispiel: Stammnummer 123.456.789, AHV-Nummer 756.1234.5678.90, Konto 01-100101-9.
97 | - Verwende das Komma, dass das deutsche Dezimalzeichen ist. Überflüssige Nullen nach dem Komma schreibst du nicht. Beispiel: 5 Millionen, 3,75 Prozent, 1,5 Kilometer, 2,25 Stunden.
98 | - Vor Franken-Rappen-Beträgen schreibst du immer «CHF». Nur nach ganzen Franken-Beträgen darfst du «Franken» schreiben. Bei Franken-Rappen-Beträgen setzt du einen Punkt als Dezimalzeichen. Anstatt des Null-Rappen-Strichs verwendest du «.00» oder lässt die Dezimalstellen weg. Z.B. 20 Franken, CHF 20, CHF 2.00, CHF 12.50, aber CHF 45,2 Millionen, EUR 14,90.
99 | - Die Anrede mit «Sie» schreibst du immer gross. Beispiel: «Sie haben».
100 | - Strukturiere den Text. Gliedere in sinnvolle Abschnitte und Absätze. Verwende Titel und Untertitel grosszügig, um den Text zu gliedern. Es kann hilfreich sein, wenn diese als Frage formuliert sind.
101 | - Stelle Aufzählungen als Liste dar.
102 | - Zeilenumbrüche helfen, Sinneinheiten zu bilden und erleichtern das Lesen. Füge deshalb nach Haupt- und Nebensätzen sowie nach sonstigen Sinneinheiten Zeilenumbrüche ein. Eine Sinneinheit soll maximal 8 Zeilen umfassen.
103 | - Eine Textzeile enthält inklusiv Leerzeichen maximal 85 Zeichen.
104 | """.strip()
105 |
106 |
107 | REWRITE_COMPLETE = """- Achte immer sehr genau darauf, dass ALLE Informationen aus dem schwer verständlichen Text in deinem verständlicheren Text enthalten sind. Kürze niemals Informationen. Wo sinnvoll kannst du zusätzliche Beispiele hinzufügen, um den Text verständlicher zu machen und relevante Inhalte zu konkretisieren."""
108 |
109 |
110 | REWRITE_CONDENSED = """- Konzentriere dich auf das Wichtigste. Gib die essenziellen Informationen wieder und lass den Rest weg."""
111 |
112 |
113 | CLAUDE_TEMPLATE_ES = """
114 | Hier ist ein schwer verständlicher Text, den du vollständig in Einfache Sprache, Sprachniveau B1 bis A2, umschreiben sollst:
115 |
116 |
117 | {prompt}
118 |
119 |
120 | Bitte lies den Text sorgfältig durch und schreibe ihn vollständig in Einfache Sprache um.
121 |
122 | Beachte dabei folgende Regeln:
123 |
124 | {completeness}
125 | {rules}
126 |
127 | Formuliere den Text jetzt in Einfache Sprache, Sprachniveau B1 bis A2, um. Schreibe den vereinfachten Text innerhalb von Tags.
128 | """.strip()
129 |
130 |
131 | CLAUDE_TEMPLATE_LS = """
132 | Hier ist ein schwer verständlicher Text, den du vollständig in Leichte Sprache, Sprachniveau A2 bis A1, umschreiben sollst:
133 |
134 |
135 | {prompt}
136 |
137 |
138 | Bitte lies den Text sorgfältig durch und schreibe ihn vollständig in Leichte Sprache um.
139 |
140 | Beachte dabei folgende Regeln:
141 |
142 | {completeness}
143 | {rules}
144 |
145 | Formuliere den Text jetzt in Leichte Sprache, Sprachniveau A2 bis A1, um. Schreibe den vereinfachten Text innerhalb von Tags.
146 | """.strip()
147 |
148 |
149 | CLAUDE_TEMPLATE_ANALYSIS_ES = """
150 | Hier ist ein schwer verständlicher Text, den du genau analysieren sollst:
151 |
152 |
153 | {prompt}
154 |
155 |
156 | Analysiere den schwer verständlichen Text Satz für Satz. Beschreibe genau und detailliert, was sprachlich nicht gut bei jedem Satz ist. Analysiere was ich tun müsste, damit der Text zu Einfache Sprache (B1 bis A2) wird. Gib klare Hinweise, wie ich den Text besser verständlich machen kann. Gehe bei deiner Analyse Schritt für Schritt vor.
157 |
158 | 1. Wiederhole den Satz.
159 | 2. Analysiere den Satz auf seine Verständlichkeit. Was muss ich tun, damit der Satz verständlicher wird? Wie kann ich den Satz in Einfacher Sprache besser formulieren?
160 | 3. Mache einen Vorschlag für einen vereinfachten Satz.
161 |
162 | Befolge diesen Ablauf von Anfang bis Ende, auch wenn der schwer verständliche Text sehr lang ist.
163 |
164 | Die Regeln für Einfache Sprache sind diese hier:
165 |
166 | {rules}
167 |
168 | Schreibe jetzt deine Analyse und gib diese innerhalb von Tags aus.
169 | """.strip()
170 |
171 |
172 | CLAUDE_TEMPLATE_ANALYSIS_LS = """
173 | Hier ist ein schwer verständlicher Text, den du genau analysieren sollst:
174 |
175 |
176 | {prompt}
177 |
178 |
179 | Analysiere den schwer verständlichen Text Satz für Satz. Beschreibe genau und detailliert, was sprachlich nicht gut bei jedem Satz ist. Analysiere was ich tun müsste, damit der Text zu Leichte Sprache (A2, A1) wird. Gib klare Hinweise, wie ich den Text besser verständlich machen kann. Gehe bei deiner Analyse Schritt für Schritt vor.
180 |
181 | 1. Wiederhole den Satz.
182 | 2. Analysiere den Satz auf seine Verständlichkeit. Was muss ich tun, damit der Satz verständlicher wird? Wie kann ich den Satz in Leichte Sprache besser formulieren?
183 | 3. Mache einen Vorschlag für einen vereinfachten Satz.
184 |
185 | Befolge diesen Ablauf von Anfang bis Ende, auch wenn der schwer verständliche Text sehr lang ist.
186 |
187 | Die Regeln für Leichte Sprache sind diese hier:
188 |
189 | {rules}
190 |
191 | Schreibe jetzt deine Analyse und gib diese innerhalb von Tags aus.
192 | """.strip()
193 |
194 |
195 | OPENAI_TEMPLATE_ES = """
196 | Du bekommst einen schwer verständlichen Text, den du vollständig in Einfache Sprache auf Sprachniveau B1 bis A2 umschreiben sollst.
197 |
198 | Beachte dabei folgende Regeln:
199 |
200 | {completeness}
201 | {rules}
202 |
203 | Schreibe den vereinfachten Text innerhalb von Tags. Gib nur Text aus, keine Markdown-Formatierung, kein HTML.
204 |
205 | Hier ist der schwer verständliche Text:
206 |
207 | --------------------------------------------------------------------------------
208 |
209 | {prompt}
210 | """.strip()
211 |
212 | OPENAI_TEMPLATE_LS = """
213 | Du bekommst einen schwer verständlichen Text, den du vollständig in Leichte Sprache auf Sprachniveau A2 bis A1 umschreiben sollst.
214 |
215 | Beachte dabei folgende Regeln:
216 |
217 | {completeness}
218 | {rules}
219 |
220 | Schreibe den vereinfachten Text innerhalb von Tags. Gib nur Text aus, keine Markdown-Formatierung, kein HTML.
221 |
222 | Hier ist der schwer verständliche Text:
223 |
224 | --------------------------------------------------------------------------------
225 |
226 | {prompt}
227 | """.strip()
228 |
229 | OPENAI_TEMPLATE_ANALYSIS_ES = """
230 | Du bekommst einen schwer verständlichen Text, den du genau analysieren sollst.
231 |
232 | Analysiere den schwer verständlichen Text Satz für Satz. Beschreibe genau und detailliert, was sprachlich nicht gut bei jedem Satz ist. Analysiere was ich tun müsste, damit der Text zu Einfache Sprache (B1 bis A2) wird. Gib klare Hinweise, wie ich den Text besser verständlich machen kann. Gehe bei deiner Analyse Schritt für Schritt vor.
233 |
234 | 1. Wiederhole den Satz.
235 | 2. Analysiere den Satz auf seine Verständlichkeit. Was muss ich tun, damit der Satz verständlicher wird? Wie kann ich den Satz in Einfache Sprache formulieren?
236 | 3. Mache einen Vorschlag für einen vereinfachten Satz.
237 |
238 | Befolge diesen Ablauf von Anfang bis Ende, auch wenn der schwer verständliche Text sehr lang ist.
239 |
240 | Die Regeln für Einfache Sprache sind diese hier:
241 |
242 | {rules}
243 |
244 | Schreibe deine Analyse innerhalb von Tags. Gib nur Text aus, keine Markdown-Formatierung, kein HTML.
245 |
246 | Hier ist der schwer verständliche Text:
247 |
248 | --------------------------------------------------------------------------------
249 |
250 | {prompt}
251 | """.strip()
252 |
253 | OPENAI_TEMPLATE_ANALYSIS_LS = """
254 | Du bekommst einen schwer verständlichen Text, den du genau analysieren sollst.
255 |
256 | Analysiere den schwer verständlichen Text Satz für Satz. Beschreibe genau und detailliert, was sprachlich nicht gut bei jedem Satz ist. Analysiere was ich tun müsste, damit der Text zu Leichte Sprache (A2 bis A1) wird. Gib klare Hinweise, wie ich den Text besser verständlich machen kann. Gehe bei deiner Analyse Schritt für Schritt vor.
257 |
258 | 1. Wiederhole den Satz.
259 | 2. Analysiere den Satz auf seine Verständlichkeit. Was muss ich tun, damit der Satz verständlicher wird? Wie kann ich den Satz in Leichte Sprache formulieren?
260 | 3. Mache einen Vorschlag für einen vereinfachten Satz.
261 |
262 | Befolge diesen Ablauf von Anfang bis Ende, auch wenn der schwer verständliche Text sehr lang ist.
263 |
264 | Die Regeln für Leichte Sprache sind diese hier:
265 |
266 | {rules}
267 |
268 | Schreibe deine Analyse innerhalb von Tags. Gib nur Text aus, keine Markdown-Formatierung, kein HTML.
269 |
270 | Hier ist der schwer verständliche Text:
271 |
272 | --------------------------------------------------------------------------------
273 |
274 | {prompt}
275 | """.strip()
276 |
--------------------------------------------------------------------------------
/_streamlit_app/utils_sample_texts.py:
--------------------------------------------------------------------------------
1 | SAMPLE_TEXT_01 = """Als Vernehmlassungsverfahren wird diejenige Phase innerhalb des Vorverfahrens der Gesetzgebung bezeichnet, in der Vorhaben des Bundes von erheblicher politischer, finanzieller, wirtschaftlicher, ökologischer, sozialer oder kultureller Tragweite auf ihre sachliche Richtigkeit, Vollzugstauglichkeit und Akzeptanz hin geprüft werden.
2 |
3 | Die Vorlage wird zu diesem Zweck den Kantonen, den in der Bundesversammlung vertretenen Parteien, den Dachverbänden der Gemeinden, Städte und der Berggebiete, den Dachverbänden der Wirtschaft sowie weiteren, im Einzelfall interessierten Kreisen unterbreitet."""
4 |
--------------------------------------------------------------------------------
/_streamlit_app/utils_understandability.py:
--------------------------------------------------------------------------------
1 | import pandas as pd
2 | from statistics import mean
3 | import os
4 | import re
5 | import spacy
6 | from spacy.language import Language
7 | from pickle import load
8 | import warnings
9 |
10 | FEATURES = {
11 | "sentence_length_mean": None,
12 | "rix": None,
13 | "vocab_a1": None,
14 | "vocab_a2": None,
15 | "vocab_b1": None,
16 | "common_word_score": None,
17 | "rix_cws": None,
18 | "rix_vocab_a1": None,
19 | "rix_vocab_a2": None,
20 | "rix_vocab_b1": None,
21 | "slm_cws": None,
22 | "slm_vocab_a1": None,
23 | "slm_vocab_a2": None,
24 | "slm_vocab_b1": None,
25 | }
26 |
27 | cefr_vocab = pd.read_parquet("data/cefr_vocab.parq")
28 |
29 | # If you want to adapt to High German and the use of `ß` rather than `ss`,
30 | # change "lemma_ch" to "lemma" in the following lines.
31 | vocab_a1 = cefr_vocab[cefr_vocab["level"] == "A1"]["lemma_ch"].values
32 | vocab_a2 = cefr_vocab[cefr_vocab["level"] == "A2"]["lemma_ch"].values
33 | vocab_b1 = cefr_vocab[cefr_vocab["level"] == "B1"]["lemma_ch"].values
34 |
35 | # Load the common word scores. This list contains the lemmas
36 | # of around ~8k most common German words.
37 | word_scores = pd.read_parquet("data/word_scores_final_0728.parq")
38 | word_scores = dict(zip(word_scores["lemma"], word_scores["score"]))
39 |
40 | # Load the fitted scaler and Ridge regressor model.
41 | with open("data/standard_scaler.pkl", "rb") as f:
42 | scaler = load(f)
43 | with open("data/ridge_regressor.pkl", "rb") as f:
44 | clf = load(f)
45 |
46 |
47 | @Language.component("additional_metrics")
48 | def _additional_metrics(doc):
49 | """Add all necessary linguistic metrics to doc.user_data.
50 |
51 | Args:
52 | doc (Doc): A spaCy Doc object.
53 |
54 | Returns:
55 | Doc: The spaCy Doc object with additional metrics in doc.user_data.
56 |
57 | """
58 |
59 | # Calculate ratio of words that are in CEFR vocabularies.
60 | doc_len = len([token for token in doc if not token.is_punct and not token.like_num])
61 |
62 | if doc_len == 0:
63 | for feature in FEATURES:
64 | doc.user_data[feature] = None
65 | return doc
66 |
67 | # Counters for B1, A2, A1 vocabularies.
68 | vocab_scores = [0, 0, 0]
69 |
70 | # Counter for common word score.
71 | doc_word_scores = 0
72 |
73 | for token in doc:
74 | lemma = token.lemma_.lower()
75 |
76 | # A word in vocab A1 is also part of the vocabulary of A2 and B1.
77 | if lemma in vocab_a1:
78 | vocab_scores[0] += 1 # B1
79 | vocab_scores[1] += 1 # A2
80 | vocab_scores[2] += 1 # A1
81 | elif lemma in vocab_a2:
82 | vocab_scores[0] += 1 # B1
83 | vocab_scores[1] += 1 # A2
84 | elif lemma in vocab_b1:
85 | vocab_scores[0] += 1 # B1
86 |
87 | if lemma in word_scores:
88 | doc_word_scores += word_scores[lemma]
89 |
90 | # Normalize scores by document length.
91 | vocab_scores = list(map(lambda x: x / doc_len, vocab_scores))
92 | doc_word_scores = doc_word_scores / doc_len
93 |
94 | doc.user_data["vocab_b1"] = vocab_scores[0]
95 | doc.user_data["vocab_a2"] = vocab_scores[1]
96 | doc.user_data["vocab_a1"] = vocab_scores[2]
97 | doc.user_data["common_word_score"] = doc_word_scores / 1000
98 |
99 | # Calculate sentence length mean.
100 | sentences_clean = [
101 | [token.text for token in sent if not token.is_punct] for sent in doc.sents
102 | ]
103 | sentence_lengths = [len(sentence) for sentence in sentences_clean]
104 | doc.user_data["sentence_length_mean"] = mean(sentence_lengths)
105 |
106 | # Calculate RIX readability index.
107 | # Reference: https://github.com/HLasse/TextDescriptives/blob/main/src/textdescriptives/components/readability.py#L146
108 | long_words = len([token for token in doc if len(token) > 6])
109 | n_sentences = len(list(doc.sents))
110 | rix = long_words / n_sentences
111 | doc.user_data["rix"] = rix
112 |
113 | # Create interaction terms.
114 | doc.user_data["rix_cws"] = doc.user_data["rix"] * doc.user_data["common_word_score"]
115 | doc.user_data["rix_vocab_a1"] = doc.user_data["rix"] * doc.user_data["vocab_a1"]
116 | doc.user_data["rix_vocab_a2"] = doc.user_data["rix"] * doc.user_data["vocab_a2"]
117 | doc.user_data["rix_vocab_b1"] = doc.user_data["rix"] * doc.user_data["vocab_b1"]
118 | doc.user_data["slm_cws"] = (
119 | doc.user_data["sentence_length_mean"] * doc.user_data["common_word_score"]
120 | )
121 | doc.user_data["slm_vocab_a1"] = (
122 | doc.user_data["sentence_length_mean"] * doc.user_data["vocab_a1"]
123 | )
124 | doc.user_data["slm_vocab_a2"] = (
125 | doc.user_data["sentence_length_mean"] * doc.user_data["vocab_a2"]
126 | )
127 | doc.user_data["slm_vocab_b1"] = (
128 | doc.user_data["sentence_length_mean"] * doc.user_data["vocab_b1"]
129 | )
130 |
131 | return doc
132 |
133 |
134 | # Make sure that the language model is installed.
135 | # Loading only the necessary components into the pipeline
136 | # speeds up the process substantially.
137 | try:
138 | nlp_pipeline = spacy.load(
139 | "de_core_news_sm", exclude=["ner", "attribute_ruler", "morphologizer", "tagger"]
140 | )
141 | except OSError:
142 | print("Downloading language model...")
143 | os.system(
144 | "pip install https://github.com/explosion/spacy-models/releases/download/de_core_news_sm-3.7.0/de_core_news_sm-3.7.0-py3-none-any.whl"
145 | )
146 | nlp_pipeline = spacy.load(
147 | "de_core_news_sm", exclude=["ner", "attribute_ruler", "morphologizer", "tagger"]
148 | )
149 |
150 | nlp_pipeline.add_pipe("additional_metrics")
151 |
152 |
153 | def _extract_features(text):
154 | """Extract syntactical and semantic features from text.
155 |
156 | Args:
157 | text (str): The text to be analyzed.
158 |
159 | Returns:
160 | pd.DataFrame: A DataFrame containing the linguistic features of the text.
161 |
162 | """
163 | doc = nlp_pipeline(text)
164 | row_features = {}
165 | for feature in FEATURES:
166 | row_features[feature] = doc.user_data[feature]
167 | return pd.DataFrame.from_dict(row_features, orient="index").T
168 |
169 |
170 | def _punctuate_lines(text):
171 | """Add a dot to lines that do not end with a dot.
172 | Remove bullet points, multiple spaces and line breaks.
173 |
174 | Args:
175 | text (str): The text to be fixed.
176 |
177 | Returns:
178 | str: The fixed text with proper punctuation.
179 | """
180 | lines = text.splitlines()
181 | lines = [x.strip() for x in lines]
182 | lines = [x for x in lines if x != ""]
183 | lines_punct = []
184 | for line in lines:
185 | # Properly punctuate lines.
186 | if line[-1] not in [".", "?", "!"]:
187 | line = line + "."
188 | # Remove bullet points.
189 | if line[0] in ["-", "•"]:
190 | line = line[1:].strip()
191 | # Remove multiple spaces.
192 | line = re.sub(r"\s+", " ", line)
193 | lines_punct.append(line)
194 |
195 | return " ".join(lines_punct)
196 |
197 |
198 |
199 | def _calculate_score(data):
200 | """Calculate the understandability of a text based on its features."""
201 | # Scale data and predict understandability.
202 | X = scaler.transform(data)
203 | understandability = 1 - clf.predict(X)[0]
204 |
205 | # Spread the score and shift it to a range from -10 to 10.
206 | score = understandability * 2.5 + 6.6
207 |
208 | # Clip to range -10 to 10.
209 | if score > 10:
210 | score = 10
211 | elif score < -10:
212 | score = -10
213 | return score
214 |
215 |
216 | def get_zix(text):
217 | """Get an understandability score from a text.
218 |
219 | Args:
220 | text (str): The text to be analyzed.
221 |
222 | Returns:
223 | float: The understandability score of the text.
224 | """
225 |
226 | # If text is not a string, raise an error.
227 | if not isinstance(text, str):
228 | warnings.warn("The given value is not of type text. Returning None.")
229 | return None
230 |
231 | # If text is an empty string return None.
232 | if text == "":
233 | warnings.warn("Input is an empty string.")
234 | return None
235 |
236 | # Spacys max_length is set to a default of 1,000,000 characters
237 | # which roughly corresponds to 10 GB RAM.
238 | if len(text) > 1_000_000:
239 | raise ValueError(
240 | """Text is too long.
241 | Please provide a text with less than 1,000,000 characters."""
242 | )
243 |
244 | text = _punctuate_lines(text)
245 | features = _extract_features(text)
246 | if features.isnull().values.any():
247 | return None
248 | score = _calculate_score(features)
249 | return score
250 |
251 |
252 | def get_cefr(zix_score):
253 | """Get the CEFR level from a ZIX score.
254 |
255 | Args:
256 | zix_score (int, float): The ZIX score of the text.
257 |
258 | Returns:
259 | str: The CEFR level of the text.
260 |
261 | """
262 | if zix_score is None:
263 | warnings.warn("The given ZIX score to the function is invalid (None).")
264 | return None
265 | if zix_score >= 4.0:
266 | return "A1"
267 | elif zix_score >= 2.0:
268 | return "A2"
269 | elif zix_score >= 0:
270 | return "B1"
271 | elif zix_score >= -2:
272 | return "B2"
273 | elif zix_score >= -4:
274 | return "C1"
275 | else:
276 | return "C2"
277 |
--------------------------------------------------------------------------------
/_streamlit_app/zix_scores.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/machinelearningZH/simply-simplify-language/35d6f9ae7fe8223a32501f27706c3cabd6b27630/_streamlit_app/zix_scores.jpg
--------------------------------------------------------------------------------
/main.py:
--------------------------------------------------------------------------------
1 | def main():
2 | print("Hello from simply-simplify-language!")
3 |
4 |
5 | if __name__ == "__main__":
6 | main()
7 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [project]
2 | name = "simply-simplify-language"
3 | version = "0.5.0"
4 | description = "Use LLMs to simplify your institutional communication."
5 | authors = [
6 | { name = "Statistisches Amt Kanton Zürich, Team Data", email = "datashop@statistik.zh.ch" },
7 | ]
8 | readme = "README.md"
9 | requires-python = ">=3.12"
10 | dependencies = [
11 | "anthropic>=0.52.2",
12 | "google-genai>=1.19.0",
13 | "mistralai>=1.8.1",
14 | "openai>=1.84.0",
15 | "pyarrow>=20.0.0",
16 | "python-docx>=1.1.2",
17 | "python-dotenv>=1.1.0",
18 | "scikit-learn>=1.7.0",
19 | "spacy>=3.8.7",
20 | "de_core_news_sm @ https://github.com/explosion/spacy-models/releases/download/de_core_news_sm-3.8.0/de_core_news_sm-3.8.0-py3-none-any.whl",
21 | "streamlit>=1.45.1",
22 | "textdescriptives>=2.8.4",
23 | ]
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | streamlit
2 | openai
3 | anthropic
4 | mistralai
5 | google-genai
6 | spacy
7 | textdescriptives
8 | python-docx
9 | python-dotenv
10 | pyarrow
11 | fastparquet
12 | scikit-learn
13 |
--------------------------------------------------------------------------------