Spaces:
Running
Running
import time | |
import tiktoken | |
import random | |
import gradio as gr | |
# HTML スタイル定義(HTML タグで色やスタイルを適用) | |
# コントラストを意識した色とオレンジ系の背景色 | |
# 1トークン用文字色 (オレンジ系背景と区別しやすい色を選択) | |
HTML_COLORS = [ | |
"#000000", # 黒 | |
"#0000cc", # 濃い青 | |
"#006600", # 濃い緑 | |
"#800080", # 紫 | |
"#cc0000", # 濃い赤 | |
] | |
# 背景色(複数トークン用、オレンジ系) | |
HTML_BACKGROUND_COLOR = "#ffe0cc" # 薄めのオレンジ | |
# アンダーバー | |
HTML_UNDERLINE_STYLE = "text-decoration: underline;" | |
def generate_html_styled_text(text: str, model_name: str = "gpt-4o", tps: int = 300): | |
""" | |
指定されたテキストを、トークンごとに定義された色のリストから順番に色を変え、 | |
複数トークンがまとめて出力される場合はオレンジ系の背景色に変え、 | |
空白の箇所にのみアンダーバーをつけて出力するためのジェネレーター (HTML スタイル使用)。 | |
Args: | |
text: 出力するテキスト文字列。 | |
model_name: トークン化に使用するモデル名。デフォルトは "gpt-4o"。 | |
tps: 1秒あたりのトークン数(Typing Per Second)。デフォルトは 300。 | |
Yields: | |
str: 逐次的に出力される HTML フラグメントを含む文字列。 | |
""" | |
try: | |
# gpt-4o のエンコーディングを取得。encoding_for_model が推奨される。 | |
encode = tiktoken.encoding_for_model(model_name) | |
except Exception as e: # より汎用的なExceptionで捕捉 | |
print(f"Warning: Model '{model_name}' encoding not found or error: {e}. Using 'cl100k_base' instead.") | |
try: | |
encode = tiktoken.get_encoding("cl100k_base") # gpt-4o が使うエンコーディング | |
except Exception as e2: | |
print(f"Error: Failed to get 'cl100k_base' encoding either: {e2}. Cannot proceed.") | |
yield f"<span style='color: red;'>Error loading encoding for model '{model_name}'.</span>" | |
return | |
text_encoded = encode.encode(text) | |
# 1トークンあたりの待機時間(秒) | |
delay_per_token = 1 / tps | |
if delay_per_token <= 0: # tpsが異常な値の場合を考慮 | |
delay_per_token = 0.01 | |
# バッファ(デコード可能なトークンを溜めておくリスト) | |
decode_buffer = [] | |
current_html = "" # Gradioに出力する現在のHTML文字列 | |
# 色を順番に選ぶためのインデックスを関数内で初期化 | |
color_index = 0 | |
for token_id in text_encoded: | |
decode_buffer.append(token_id) | |
time.sleep(delay_per_token) | |
# バッファの内容をデコードしてみる | |
try: | |
decoded_text_candidate = encode.decode(decode_buffer) | |
# デコードに成功し、かつ不正な文字(�)が含まれていない場合 | |
if "�" not in decoded_text_candidate: | |
decoded_text = decoded_text_candidate # 確定したデコード済みテキスト | |
# デコードされたテキストを HTML エスケープ(<, >, &などを変換) | |
# Spanタグの中にそのまま入れるとタグとして解釈される可能性があるため | |
# スペースも に変換して複数スペースが潰れないようにする | |
# 改行文字も <br> に変換(出力エリアがTextboxでないHTMLのため、自動改行されない場合がある) | |
escaped_text = decoded_text.replace("&", "&").replace("<", "<").replace(">", ">").replace(" ", " ").replace("\n", "<br>") | |
# デコードされたテキストに空白のみが含まれているか判定 (元のデコード済みテキストで判定) | |
# ただし、HTMLエスケープされたスペース ( ) や改行 (<br>) は空白とみなさないように注意 | |
# 元の decoded_text を strip() するのが適切 | |
is_whitespace_only = decoded_text.strip() == "" | |
style = "" | |
if is_whitespace_only: | |
# 空白のみの場合、アンダーバーのスタイルを適用 | |
style += HTML_UNDERLINE_STYLE | |
# 空白の背景色はつけない | |
elif len(decode_buffer) > 1: # 複数トークン (かつ空白でない) | |
# 複数トークンの場合、オレンジ系背景色スタイルを適用 | |
style += f"background-color: {HTML_BACKGROUND_COLOR};" | |
else: # 1トークン (かつ空白でない) | |
# 1トークンのみの場合、定義済みリストから順番に文字色スタイルを適用 | |
color = HTML_COLORS[color_index] | |
style += f"color: {color};" | |
# 次の色へインデックスを進める(リストの最後なら最初に戻る) | |
color_index = (color_index + 1) % len(HTML_COLORS) | |
# スタイルを適用した span タグを生成 | |
if style: | |
# ここで改行コードを処理している場合、<br> は span の外に出すべきかもしれないが | |
# トークン化の境界によっては span の中に <br> が入る可能性もある。 | |
# 簡単のため、エスケープ済みのテキストをそのまま span に入れる。 | |
# これにより、改行自体にはスタイルが適用されないが、直前のテキストにスタイルが付く形になる。 | |
current_html += f"<span style='{style}'>{escaped_text}</span>" | |
else: | |
# スタイルがない場合(起こりにくいが念のため) | |
current_html += escaped_text | |
decode_buffer = [] # バッファをクリア | |
yield current_html # Gradioに出力を反映 | |
except Exception as e: | |
# デバッグ用にエラーをプリント | |
# print(f"Decode error or processing error: {e}") | |
# デコードに失敗した場合、バッファに保持したまま次のトークンへ | |
pass | |
# ループ終了後にバッファに残っているトークンがあれば出力 | |
if decode_buffer: | |
remaining_text_candidate = encode.decode(decode_buffer) | |
if "�" not in remaining_text_candidate: | |
remaining_text = remaining_text_candidate | |
# 残ったテキストを HTML エスケープ | |
escaped_remaining_text = remaining_text.replace("&", "&").replace("<", "<").replace(">", ">").replace(" ", " ").replace("\n", "<br>") | |
is_whitespace_only_remaining = remaining_text.strip() == "" | |
style = "" | |
if is_whitespace_only_remaining: | |
# 残ったトークンが空白のみの場合、アンダーバーのスタイルを適用 | |
style += HTML_UNDERLINE_STYLE | |
else: | |
# 残ったトークンが空白でない場合、複数トークンとしてオレンジ系背景色スタイルを適用 | |
style += f"background-color: {HTML_BACKGROUND_COLOR};" | |
# スタイルを適用した span タグを生成 | |
if style: | |
current_html += f"<span style='{style}'>{escaped_remaining_text}</span>" | |
else: | |
current_html += escaped_remaining_text | |
yield current_html # Gradioに出力を反映 | |
# 最後にバッファをクリアしておかないと、次回の実行時に前回の残りが影響する可能性がある | |
decode_buffer = [] | |
pass # 最後の yield はループ内で行われるため不要 | |
# Example テキストのリスト | |
# インデントを含む一般的な日本語の例を新しく作成 | |
example_texts = [ | |
"""会議の議題と決定事項: | |
1. プロジェクトA進捗報告 | |
- 山田さん: タスクX完了 (順調) | |
- 佐藤さん: タスクY遅延 (原因: 外部連携) | |
- 対策: 代替手段の検討 (来週決定) | |
2. 来週イベント準備 | |
- 会場設営: 担当者未定 (要早急な調整) | |
- 配布資料: 印刷部数確定 (明日まで) | |
3. その他 | |
- 次回会議日程: 来週水曜日""", | |
"""Python 関数例: | |
def calculate_total(price, tax_rate=0.08): | |
# 税込み価格を計算する関数 | |
total = price * (1 + tax_rate) | |
return total | |
# 使用例 | |
item_price = 1000 | |
total_price = calculate_total(item_price) | |
print(f"税込み価格: {total_price}円")""", | |
"""旅行計画 (3日間 京都): | |
日程: | |
1日目: | |
- 午前: 京都駅到着 | |
- 午後: 金閣寺、嵐山散策 | |
- 夜: 祇園で夕食 | |
2日目: | |
- 終日: 伏見稲荷大社、清水寺、東山エリア観光 | |
3日目: | |
- 午前: 錦市場で食べ歩き、お土産購入 | |
- 午後: 京都駅出発 | |
宿泊エリア: 京都駅周辺 もしくは 四条河原町周辺""", | |
"""読書カフェ名候補: | |
- 静寂文庫 | |
- 栞の時間 | |
- 書籍の灯台 | |
- コトバノオト | |
- ブックノアール""", | |
"""簡単な俳句: | |
夏草や | |
兵どもが | |
夢の跡""", | |
"""英語翻訳例: | |
"The quick brown fox jumps over the lazy dog." | |
-> "素早い茶色のキツネは怠惰な犬を飛び越える。" | |
(これは、ほぼ全てのアルファベットを含むことで知られるパングラムです)""", | |
] | |
# Gradio インターフェースの定義 | |
# theme=gr.themes.Base() を指定して、白を基調とした基本テーマを適用 | |
# Darkモードはデフォルトで無効 (Gradio 4.0以降は自動でOS設定に従う可能性があるため、明示的に無効化する場合は launch() の前に環境変数を設定するなどが必要な場合も) | |
# theme='base' は theme=gr.themes.Base() と同じ意味で使われることがあります。 | |
with gr.Blocks(theme=gr.themes.Base()) as demo: | |
gr.Markdown("## テキスト タイピング効果") | |
with gr.Row(): # 入力エリアと出力エリアを左右に分割 | |
with gr.Column(): # 左側の入力エリア | |
gr.Markdown("### 入力") | |
input_text = gr.Textbox(label="入力テキスト", lines=10, value="", placeholder="ここにテキストを入力してください...", show_label=False) # labelはタイトルで表示するので非表示に | |
# Example コンポーネントをAccordionの中に配置 | |
# open=False でデフォルトで畳まれた状態にする | |
with gr.Accordion("入力例", open=False): | |
examples = gr.Examples( | |
examples=example_texts, | |
inputs=input_text, # クリックされた例が input_text に入る | |
label="入力例" # Accordionのタイトルがあるので、labelは非表示に | |
) | |
tps_slider = gr.Slider(minimum=1, maximum=500, value=100, label="タイピング速度 (トークン/秒)") | |
run_button = gr.Button("実行") | |
with gr.Column(): # 右側の出力エリア | |
gr.Markdown("### 出力") | |
# 出力コンポーネネント (HTMLを使用) | |
# HTMLコンポーネントに行数を指定するのは難しいので、シンプルな表示に | |
# 高さを調整したい場合は、GradioのCSSをいじるか、HTMLコンポーネントをdivで囲むなどの工夫が必要 | |
# 出力エリアを固定高さにする例 (CSSを使用): | |
# output_html = gr.HTML(elem_classes=["fixed-height-output"]) | |
output_html = gr.HTML() | |
run_button.click( | |
fn=generate_html_styled_text, | |
inputs=[input_text, gr.State("gpt-4o"), tps_slider], # model_name は固定、tpsはスライダーから取得 | |
outputs=output_html # 出力先を HTML コンポーネントに変更 | |
) | |
# CSSを追加して出力エリアの高さを固定する場合 (オプション) | |
# demo.css = """ | |
# .fixed-height-output { | |
# height: 300px; /* 任意の高さを指定 */ | |
# overflow-y: auto; /* 縦スクロールを可能にする */ | |
# border: 1px solid #ccc; /* 枠線を付けて分かりやすくする */ | |
# padding: 10px; | |
# } | |
# """ | |
# Gradio アプリケーションの起動 | |
# launch(debug=True) にすると、エラーの詳細が表示されます。 | |
# dark=False は推奨されない場合があります。theme='base' でライトテーマを適用するのが標準的です。 | |
# 環境変数 GRADIO_DARK_MODE を設定することで制御することも可能です。 | |
# 例: GRADIO_DARK_MODE=0 python your_script.py | |
demo.launch() |