42
50

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ChatGPT のようなヌルヌルした増分テキスト表示をやってみたい

Last updated at Posted at 2024-04-24

タイトルの通りです。Python の FastAPI で簡単なサンプルアプリを作成しました。

実行例

streaming.gif

FastAPI のコード

HTTP は昔からストリームなので HTTP レスポンスのボディを逐次、送受信することは以前から可能でした。でも HTTPヘッダの Content-Length がネックでした。HTTPヘッダは最初に返さないといけないのでその時点でコンテンツサイズが未定の場合は Content-Length を設定できないからです。

HTTP1.1 が全盛の頃はコンテンツを細切れ(チャンク)にして送信していましたが今は、楽に実装できるようになりました。FastAPI は、StreamingResponse を使います。ブラウザ側は、HTTPレスポンスの ReadableStreamgetReader() して Reader を取得します。

main.py
import asyncio
from fastapi import FastAPI
from fastapi.responses import HTMLResponse, StreamingResponse

app = FastAPI()


# HTTPストリーミングレスポンスを返す
@app.get("/streaming")
async def streaming():
    text = '''吾輩は猫である。名前はまだ無い。どこで生れたかとんと見当けんとうがつかぬ。
何でも薄暗いじめじめした所でニャーニャー泣いていた事だけは記憶している。
吾輩はここで始めて人間というものを見た。しかもあとで聞くとそれは書生という人間中で一番獰悪どうあくな種族であったそうだ。
この書生というのは時々我々を捕つかまえて煮にて食うという話である。
'''
    async def generate_data():
        for c in text:
            yield c
            await asyncio.sleep(0.05)

    return StreamingResponse(generate_data(), media_type="text/plain")


# HTMLを返す
@app.get("/")
async def index():
    html = '''
<html>
    <body>
        <h1>Hello Streaming!</h1>
        <div id="foo" style="white-space: pre-wrap;"></div>

        <script>
        document.addEventListener('DOMContentLoaded', async function () {
            const div = document.getElementById("foo");
            const response = await fetch("/streaming", {method: "GET"});
            if (!response.ok) {
                throw new Error(`HTTP error! status: ${response.status}`);
            }
            const decoder = new TextDecoder();
            const reader = response.body.getReader();
            while (true) {
                const { done, value } = await reader.read();
                if (done) break;
                const text = decoder.decode(value);
                div.textContent = div.textContent + text;
            }
        });
        </script>
    </body>
</html>
'''
    return HTMLResponse(content=html)

実行方法

フォルダ構成

./
├── docker-compose.yml
├── Dockerfile
├── requirements.txt
└── app/
    └── main.py

Dockerfile

Dockerfile
FROM public.ecr.aws/docker/library/python:3.12-slim
WORKDIR /app
COPY ./requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt
COPY app/. ./
EXPOSE 8000
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]

docker-compose.yml

docker-compose.yml
services:
  streaming:
    image: streaming:0.1
    build:
      context: .
      dockerfile: ./Dockerfile
    container_name: streaming
    working_dir: /app
    ports:
      - "0.0.0.0:8000:8000"
    volumes:
      - ./app:/app

requirements.txt

requirements.txt
fastapi
uvicorn[standard]

実行

docker compose build

docker compose up -d

ブラウザで http://localhost:8000/ を開く。

以上です.

42
50
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
42
50

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?