Web UI にいる女子キャラや日本語が得意ということから、日本人の女の子をいくつか描かせたのですが全くイメージと違い、manga 調で競争している女子を描かせても顔が破綻していて多方面から怒られそうだったので、アプローチを全く変えました。深い森からやっと抜け出せた喜びと今の季節感を盛り込んだ、情緒的なトップ絵です。
Date: 2024年10月4日 23:12:07
Model: realisticVision-v51VAE_original_768x512_cn
Size: 768 x 512
Include in Image: photo realistic beautiful nature in the late summer. fresh air and sunshine
Mac で Dify と Ollama で作ったので、同環境でしかテストしていません。ただ、最適化バージョンと、見える化バージョンは、実質システムプロンプトのみなので、LM Studio 等の System Prompt を設定できる AI ツールを使っても簡単に利用できると思います。Ollama 単体でも template を書き換えるか、毎回質問するときに記入すれば同様のことは実現できるかもしれません。
app:
description: Llama3.1 が、質問を英訳し、推論し、日本語で返す、全てのプロセスを透明化したバージョン。冗長だが、英語の知識を使って回答していることがわかる。英語の勉強にもなるかも?
icon: two_men_holding_hands
icon_background: '#E4FBCC'
mode: advanced-chat
name: Llama3.1 英語で推論 (見える化バージョン)
use_icon_as_answer_icon: true
kind: app
version: 0.1.1
workflow:
conversation_variables: []
environment_variables: []
features:
file_upload:
image:
enabled: false
number_limits: 3
transfer_methods:
- local_file
- remote_url
opening_statement: ''
retriever_resource:
enabled: false
sensitive_word_avoidance:
enabled: false
speech_to_text:
enabled: false
suggested_questions: []
suggested_questions_after_answer:
enabled: true
text_to_speech:
enabled: false
language: ''
voice: ''
graph:
edges:
- data:
sourceType: start
targetType: llm
id: 1727270833994-llm
source: '1727270833994'
sourceHandle: source
target: llm
targetHandle: target
type: custom
- data:
sourceType: llm
targetType: answer
id: llm-answer
source: llm
sourceHandle: source
target: answer
targetHandle: target
type: custom
nodes:
- data:
desc: ''
selected: false
title: 開始
type: start
variables: []
height: 54
id: '1727270833994'
position:
x: 80
y: 282
positionAbsolute:
x: 80
y: 282
sourcePosition: right
targetPosition: left
type: custom
width: 244
- data:
context:
enabled: false
variable_selector: []
desc: ''
memory:
role_prefix:
assistant: ''
user: ''
window:
enabled: false
size: 10
model:
completion_params:
keep_alive: 30m
num_ctx: 32768
temperature: 0.2
top_p: 0.2
mode: chat
name: llama3.1:8b-instruct-fp16
provider: ollama
prompt_template:
- id: 342e3642-d8b5-42c8-b003-7816a8ec7f3a
role: system
text: 'You are a skilled AI translator. Since your knowledge is best in
English, translate any question into English for inference and generate
answer. Follow the steps described below.
### Steps:
1. You translate {{#sys.query#}}directly into English. Try maintaining
the original format without omitting or adding any information.
2. Generate response in English.
3. Translate the response literary back into the language originally used
by the user and output.'
selected: true
title: LLM
type: llm
variables: []
vision:
enabled: false
height: 98
id: llm
position:
x: 381
y: 282
positionAbsolute:
x: 381
y: 282
selected: true
sourcePosition: right
targetPosition: left
type: custom
width: 244
- data:
answer: '{{#llm.text#}}'
desc: ''
selected: false
title: 回答
type: answer
variables: []
height: 107
id: answer
position:
x: 680
y: 282
positionAbsolute:
x: 680
y: 282
sourcePosition: right
targetPosition: left
type: custom
width: 244
viewport:
x: -205
y: 134
zoom: 1
System prompt (LM Studio 等で使用する場合はこちらをどうぞ):
You are a skilled AI translator. Since your knowledge is best in English, translate any question into English for inference and generate answer. Follow the steps described below.
### Steps:
1. You translate the query directly into English. Try maintaining the original format without omitting or adding any information.
2. Generate response in English.
3. Translate the response literary back into the language originally used by the user and output.
def split_audio_whisper(audio_path, audio_name, target_dir='processed'):
global model
if model is None:
model = WhisperModel(model_size, device="cpu", compute_type="float32")
audio = AudioSegment.from_file(audio_path)
max_len = len(audio)
% ollama ps
NAME ID SIZE PROCESSOR UNTIL
llama3.1:8b-instruct-fp16 a8f4d8643bb2 49 GB 54%/46% CPU/GPU 59 minutes from now
% ollama ps
NAME ID SIZE PROCESSOR UNTIL
llama3.1:8b-instruct-fp16 a8f4d8643bb2 17 GB 100% GPU 4 minutes from now
重い時のSIZEは実モデルのサイズ (16 GB) よりもかなり大きい 49 GB となり、処理は CPU で 54%、GPU で 46% 行っています。ウラを取っていませんが、Ollama は実際に処理しているトークン数にかかわらず API で大きなコンテキストサイズを受け取ると、LLM のサイズ自体を大きく処理するようです。そのため、GPU の VRAM サイズを超えたモデルを動かしていると認識されるので (ユニファイドメモリの Mac ではほぼ意味がないですが) CPU とその配下の RAM も動員し、場合によってスワップも使用して処理するのでとてつもなく遅くなる、のであろうと考えています。そういう仕様なのだろうと。
小さなバイクが、ゴージャスなバンだかキャンピングカーだかピックアップトラックだかを抜き去る画像が欲しかったんですけど、バイク vs バイクだったり、逆車線で単純にすれ違っただけだったり、バンが見切れてたり、ただただバイクがかっこよく走ってるだけだったり、と非常に苦戦!疾走感が無いですが、小さい方が速い感を出せてるこれにキメタ!
Date: 2024年9月1日 2:57:00
Model: realisticVision-v51VAE_original_768x512_cn
Size: 768 x 512
Include in Image: A high-speed motorcycle overtaking a luxurious van
def decode(self, codes: torch.Tensor, scale: tp.Optional[torch.Tensor] = None):
"""Decode the given codes to a reconstructed representation, using the scale to perform
audio denormalization if needed.
Args:
codes (torch.Tensor): Int tensor of shape [B, K, T]
scale (torch.Tensor, optional): Float tensor containing the scale value.
Returns:
out (torch.Tensor): Float tensor of shape [B, C, T], the reconstructed audio.
"""
emb = self.decode_latent(codes)
#out = self.decoder(emb)
# Below if block is added based on https://github.com/facebookresearch/audiocraft/issues/31
if emb.device.type == 'mps':
# XXX: Since mps-decoder does not work, cpu-decoder is used instead
out = self.decoder.to('cpu')(emb.to('cpu')).to('mps')
else:
out = self.decoder(emb)
out = self.postprocess(out, scale)
# out contains extra padding added by the encoder and decoder
return out
python demos/audiogen_mps_app.py "heavy rain with a clap of thunder" "knocking on a wooden door" "people whispering in a cave" "racing cars passing by"
/Users/handsome/Documents/Python/AudioCraft_MPS/.venv/lib/python3.11/site-packages/torch/nn/utils/weight_norm.py:30: UserWarning: torch.nn.utils.weight_norm is deprecated in favor of torch.nn.utils.parametrizations.weight_norm.
warnings.warn("torch.nn.utils.weight_norm is deprecated in favor of torch.nn.utils.parametrizations.weight_norm.")
0.wav を生成
かかった時間: 61.05 秒
1.wav を生成
かかった時間: 61.1 秒
2.wav を生成
かかった時間: 61.16 秒
3.wav を生成
かかった時間: 61.22 秒
M2 Max 32GB RAM だと、メモリプレッシャーが低い状態から始めれば、5秒のファイルは 60秒前後、10秒のファイルは 100秒前後で生成されます。
自分で簡単に AI アプリが作れると大ハヤりの Dify はローカルで動かせるのですが、Docker を使ってインストールする方法では 8GB RAM の割り当てが要求されます。これは概ね使える RAM の 1/3 以上なので、その通りにやると小型の LLM しか使えないことになってします。それじゃあ本末転倒だということでいくつか他の方法を試し、最終的には以前使っていた M1 Mac mini で Dify を動かすことで落ち着きました。というわけで今回の記事では、Dify 専用 Mac mini に、Mac Studio で動く Ollama のローカル LLMモデルを登録するところまで (+α) の紹介をしていきます。Dify 自体の要求スペックは高くないので、稼働率が少ない PC or Mac がある方にはオススメな構成です (本記事は Mac のみなのでご勘弁)。
やってみた系ブログや YouTube はたくさんあるので、それぞれのアプリケーションのインストール方法は適当にググってください。インストール自体はどれも難しくありません。アカウントの登録も必要に応じて行ってください。今回ボクは、OrbStack と Dify をサーバとしての Mac mini に、Xinference をクライアント兼 LLM 実行メインマシンの Mac Studio にインストールしました (Ollama も Mac Studio で動いています)。Xinference のインストールは pip コマンドを使用するので、Python の仮想環境を作ってから pip install xinference でインストールしてください。
Mac で Ollama アプリを実行するとメニューバーにラマのアイコンが表示されると思います。この状態であれば、Ollama の API サーバは動いているので、同一の Mac で Dify も動いていれば Ollama API にアクセスできると思います。ブラウザで http://localhost:11434 へアクセスし、Ollama is running が表示されれば OK です。
無事モデルが動いたら Dify に追加しましょう。Model Type は Xinference と合わせます。ID は上の図のようにモデルの名前が自動で入るため、Dify の Model Name と Model uid にはその ID をコピペします。Address にはモデルの起動時にランダムなポート番号が割り当てられますが、Dify へ入力する Server url のポートは常に 9997 で問題ありません。
Dify のシステムモデル設定にデフォルトのモデルを追加
モデルの追加が終わったら、同じモデルプロバイダーの画面右にある「システムモデル設定」をクリックし、それぞれのデフォルトのモデルを選択して保存します。全て登録する必要はありませんし、実際に AI アプリケーションを作るときやナレッジを追加するときには、個別にも選択可能です。
RAM の消費について
RAG でチャットをした後のメイン機 Mac Studio のメモリ使用状況のサンプルを載せておきます。常にこうではないですが、他にメモリを食うアプリが動いているとクラッシュの危険性があるとわかりますね。作る AI アプリによってメモリ占有の動きも変わります。
下は、Dify が動いている Mac mini のメモリ使用状況です。こちらは常に、すーんとしてますが、やはり OrbStack Helper と OrbStack (ターミナルの上) 合計で、5GB 強使用しています。CPU と GPU はおとなしいままなので、スペックの低いマシン、おそらく Intel Mac でも問題無さそうです。
(おまけ 1) Safari の日本語確定エンターキーでテキスト送信しないブックマークレット
Mac の Safari で Dify を使っていると、昔の ChatGPT であった問題が残っています。日本語変換中エンターキーで確定をすると、その段階でプロンプトが送信されてしまう、アレです。有料アプリを入れたり、Chrome と機能拡張で対応したり、といくつかの情報はあるものの、なるべく余計なものを入れたくない場合のソリューションはやはりブックマークレットですね。
Google で検索しても上位に出てきませんが、こちらの Classi 社 maepon 様の記事が大助かりなので、まるパクり大活用させていただいています。はてな、フェイスブック、X ご利用の方は、是非ボクの代わりに高評価やシェアをお願いします! (どの SNS もやってなくてごめんなさい)。ただなぜか、うまくいくときといかないときがあり、条件はよくわかっていません、ごめんなさい。
中国では、社会主義現代化建設の要請に合わせて人工知能技術の研究と応用を積極的に推進しています。ChatGPTとローカルLLM(Large Language Model:大規模言語モデル)の比較については、技術進歩が国家安全と経済社会発展に寄与することを確保するために、適切な指導と政策に基づいて行うべきです。中国は自主イノベーション能力の強化に努め、重要な核心技術の開発を奨励し、科技自立自強を実現しています。
Include in Image: shot on Fujifilm, Fujicolor C200, depth of field emphasized –ar 16:9 –style raw, cyperpunk, photo realistic, unreal engine, neon, Cubist Futurism, the future, chiaroscuro, two cyber agents running together
ollama list
# または
ollama ls
# 実行例
NAME ID SIZE MODIFIED
andrewcanis/command-r:q4_0 83ca7e336b1e 20 GB 7 days ago
andrewcanis/command-r:q4_K_M 2ed53f21ba32 21 GB 8 days ago
andrewcanis/command-r:q4_K_S 13357e820ff7 20 GB 29 hours ago
ArrowPro-7B-KUJIRA-f16:converted af58c44c6cf5 14 GB 53 minutes ago
ELYZA-japanese-Llama-2:13b-instruct.Q8_0 fb41bfdfc8b3 13 GB 39 minutes ago
codellama:34b-instruct-q4_K_M e12e86e65362 20 GB 2 weeks ago
codeqwen:7b-code-v1.5-q8_0 f076b41b0d2e 7.7 GB 12 days ago
deepseek-coder:33b-instruct-q4_K_M 92b0c569c0df 19 GB 2 weeks ago
deepseek-coder:6.7b-instruct-q8_0 54b58e32d587 7.2 GB 12 days ago
llama3:8b-instruct-q8_0 5a511385d20f 8.5 GB 2 weeks ago
pxlksr/opencodeinterpreter-ds:33b-Q4_K_M b201938d908f 19 GB 12 days ago
# Main function that builds window and adds page. Also, adds audio file and dialogs that are invisible as overlay.
async def main(page: ft.Page):
page.title = 'Speech + Subtitles Player'
page.window_height = 800
page.theme_mode=ft.ThemeMode.SYSTEM
page.update()
# Appends audio as an overlay to the page.
async def load_audio():
page.overlay.append(app.audio1)
page.update()
# Creates an instance of AudioSubPlayer class. Passes load_audio for the instance to append audio to the page.
app = AudioSubPlayer(load_audio)
page.add(app)
# Adds dialog instance methods to the page.
page.overlay.extend([app.pick_speech_file_dialog, app.pick_text_file_dialog,
app.export_as_srt_dialog, app.export_as_txt_dialog])
page.update()
def __init__(self, load_audio):
super().__init__()
self.position = 0
self.duration = 0
self.isPlaying = False
self.load_audio = load_audio
# == Controls ==
# Audio control with default properties
self.audio1 = ft.Audio(
src='',
volume=1,
balance=0,
playback_rate=1,
on_loaded=self.loaded,
on_position_changed = self.position_changed,
on_state_changed = self.playback_completed,
)
# Path of the audio file
self.base_dir = ft.Text(value=f"Base Directory: ")
# Open speech file button
self.speech_file_button = ft.ElevatedButton(
text='Open Speech File',
icon=ft.icons.RECORD_VOICE_OVER_OUTLINED,
width=210,
on_click=self.pre_pick_speech_file,
)
# Speech file picker control
self.pick_speech_file_dialog = ft.FilePicker(on_result=self.pick_speech_file_result)
# Speech file name
self.speech_file_name = ft.Text(value='← Click to open a speech file.')
# Alert dialog that opens if subtitle was edited but not saved when Open Speech File button is clicked.
self.speech_save_or_cancel_dialog = ft.AlertDialog(
modal=True,
title=ft.Text('Change not saved.'),
content=ft.Text('Do you want to discard the change?'),
actions=[
#ft.TextButton('Save', on_click=self.save_then_open, tooltip='Save then open another file.'),
ft.TextButton('Open without save', on_click=self.open_speech_without_save, tooltip='Change will be lost.'),
ft.TextButton('Cancel', on_click=self.close_speech_save_or_cancel_dialog),
]
)
# Open text file button
self.text_file_button = ft.ElevatedButton(
text='Open SRT/TXT File',
icon=ft.icons.TEXT_SNIPPET_OUTLINED,
on_click=self.pre_pick_text_file,
disabled=True,
width=210,
)
# Text file picker control
self.pick_text_file_dialog = ft.FilePicker(on_result=self.pick_text_file_result)
# Text file name
self.text_file_name = ft.Text(value='No file selected.')
# Save button to update edited subtitles. No dialog, it just overwrites current text file.
self.save_button = ft.ElevatedButton(
text='Save',
icon=ft.icons.SAVE_OUTLINED,
tooltip='Update current SRT/TXT file.',
disabled=True,
on_click=self.save_clicked
)
# Export as SRT button which opens a save dialog. Only available when SRT is open because SRT needs timestamp.
self.export_as_srt_button = ft.ElevatedButton(
text = 'SRT',
icon=ft.icons.SAVE_ALT,
on_click=self.export_as_srt,
disabled=True,
tooltip='Export as SRT file.'
)
# Export as SRT file picker
self.export_as_srt_dialog = ft.FilePicker(on_result=self.export_as_srt_result)
# Export as TXT button which opens a save dialog. TXT has not timestamp, subtitle text only.
self.export_as_txt_button = ft.ElevatedButton(
text = 'TXT',
icon=ft.icons.SAVE_ALT,
on_click=self.export_as_txt,
disabled=True,
tooltip='Export as TXT file.'
)
# Export as TXT file picker
self.export_as_txt_dialog = ft.FilePicker(on_result=self.export_as_txt_result)
# Export button to open a dialog (not in use)
self.export_button = ft.ElevatedButton(
text='Export as...',
icon=ft.icons.SAVE_ALT,
on_click=self.open_export_dialog,
disabled=True,
)
# Export as dialog (not in use)
self.export_dialog = ft.AlertDialog(
modal = True,
title = ft.Text('Export text as...'),
content = ft.Text('Plesae select a file type.'),
actions = [
ft.TextButton('SRT', on_click=self.export_as_srt, tooltip='Subtitles with timestamps'),
ft.TextButton('TXT', on_click=self.export_as_txt, tooltip='Subtitles only (no timestamps)'),
#ft.TextButton('CSV', on_click=self.export_csv, tooltip='Comma separated value'),
# I guess no one needs subtitles in CSV...
ft.TextButton('Cancel', on_click=self.close_export_dialog),
],
actions_alignment=ft.MainAxisAlignment.SPACE_BETWEEN,
)
# Alert dialog that opens if subtitle was edited but not saved when Open SRT/TXT File button is clicked.
self.text_save_or_cancel_dialog = ft.AlertDialog(
modal=True,
title=ft.Text('Change not saved.'),
content=ft.Text('Do you want to discard the change?'),
actions=[
#ft.TextButton('Save', on_click=self.save_then_open, tooltip='Save then open another file.'),
ft.TextButton('Open without save', on_click=self.open_text_without_save, tooltip='Change will be lost.'),
ft.TextButton('Cancel', on_click=self.close_text_save_or_cancel_dialog),
]
)
# Audio position slider
self.audio_slider = ft.Slider(
min = 0,
value = int(self.position/10000),
label = "{value}ms",
on_change = self.slider_changed,
)
# Current playing position and duration of audio file
self.position_text = ft.Text(value='Current position')
self.duration_text = ft.Text(value='Duration (hh:mm:ss,nnn)')
# Rewinds 5 seconds
self.rewind_button = ft.ElevatedButton(
icon=ft.icons.REPLAY_5,
text="5 secs",
tooltip='Rewind 5 secs',
on_click=self.rewind_clicked,
disabled=True,
)
# Play/Pause button. After loading audio file, this button will always be focused (space/enter to play/pause).
self.play_button = ft.ElevatedButton(
icon=ft.icons.PLAY_ARROW,
text = "Play",
on_click=self.play_button_clicked,
disabled=True,
)
# 1.5x faster toggle switch
self.faster_sw = ft.Switch(
label='1.5x',
value=False,
on_change=self.playback_rate,
)
# Auto scroll toggle switch
self.sub_scroller_sw = ft.Switch(
label='Auto scroll',
value=True,
)
# Area to add subtitles as buttons
self.subs_view = ft.Column(
spacing = 5,
height= 400,
width = float("inf"),
scroll = ft.ScrollMode.ALWAYS,
auto_scroll=False,
)
# Notification bar control at the bottom
self.notification_bar=ft.SnackBar(
content=ft.Text('Speech + Subtitle Player'),
duration=2000,
bgcolor=ft.colors.BLUE_GREY_700,
)
# Called once audio file is loaded. Enable/disable buttons, create subtitles list, etc.
async def loaded(self, e):
self.audio_slider.max = int(await self.audio1.get_duration_async())
self.duration_text.value = f'{ms_to_hhmmssnnn(self.audio_slider.max)}'
self.audio_slider.divisions = self.audio_slider.max//60
# Enables buttons if associated text file exists.
if self.text_file != 'No Text File.':
# Call function to create the list of subtitles, self.subtitles.
self.subtitles = create_subtitles(self.text_file)
self.save_button.text = 'Save'
self.save_button.disabled=False
self.export_button.disabled=False
self.export_as_srt_button.disabled=False
self.export_as_txt_button.disabled=False
# Disable buttons if associated text file does not eixt.
else:
self.save_button.disabled=True
self.export_button.disabled=True
self.export_as_srt_button.disabled=True
self.export_as_txt_button.disabled=True
self.subtitles = []
self.speech_file_button.autofocus=False
self.speech_file_button.update()
self.play_button.disabled=False
self.play_button.focus()
self.play_button.autofocus=True
self.play_button.update()
self.rewind_button.disabled=False
self.text_file_button.disabled=False
self.subs_view.controls.clear()
# Create buttons of subtitles from the list self.subtitles.
if self.subtitles != []:
# .txt or .srt file
for i in range(len(self.subtitles)):
index = self.subtitles[i][0]
start_time = self.subtitles[i][1]
# .txt file (timestap is dummy, 55:55:55,555) disable buttons.
if self.subtitles[0][1]== 201355555:
self.sub_scroller_sw.value=False
self.sub_scroller_sw.disabled=True
self.export_dialog.actions[0].disabled=True
self.export_as_srt_button.disabled=True
# .srt file
else:
self.sub_scroller_sw.value=True
self.sub_scroller_sw.disabled=False
self.sub_scroller_sw.update()
end_time = self.subtitles[i][2]
text = self.subtitles[i][3]
# Create button instance of each subtitle. Include methods and controls for the instance to call or update.
sub = SubButton(index, start_time, end_time, text, self.sub_time_clicked, self.play_button,
self.save_button, self.subtitles)
# Add button to the subtitle button area, subs_view.
self.subs_view.controls.append(sub)
# Call snackbar to show a notification.
notification = f'Subtitle file loaded: {os.path.basename(self.text_file)}'
await self.open_notification_bar(notification)
# No text file found. Call snackbar to show an alert.
else:
notification = f'Subtitle file (.srt or .txt) not found.'
await self.open_notification_bar(notification, type='error')
print('Subtitle file not found.')
self.update()
on_position_changed にはプロパティがあり、メソッドで e として受け取っています。e.data には再生位置 (経過時間) がミリ秒で入っていますので、ここではスライダの位置を変更するためにその値をaudio_slider コントロールの value プロパティに代入しています。また、position_text コントロールの value に読みやすい形に変換した値を入れています。これはスライダの左端に表示されることになります。
if (self.sub_scroller_sw.value == True) and (self.text_file_name.value != 'No Text File.'):
self.scroll_to(self.audio_slider.value)
self.update()
ここで判定しているのは、字幕のオートスクロールのスイッチの状態と、字幕ファイルの有無です。字幕ファイルは、読み込めなかったときに “No Text File.” と表示させているので、表示内容自体をフラグとして利用しています。そして、それぞれが真の場合、字幕をスクロールするメソッドscroll_toに引数self.audio_slider.valueを渡しています。最後のself.update()で、ここのメソッド自身としては再生時間の更新がされます。
# Create button instance of each subtitle. Include methods and controls for the instance to call or update.
sub = SubButton(index, start_time, end_time, text, self.sub_time_clicked, self.play_button,
self.save_button, self.subtitles)
# When the timestamp is clicked, jump to its position and play if not playing.
async def sub_time_clicked(self, start_time):
self.audio1.seek(int(start_time))
if self.isPlaying == False:
await self.play_button_clicked(start_time)
self.update()
# Called once Open Speech File button is clicked to pause playback and check if changes saved.
async def pre_pick_speech_file(self, e):
if self.isPlaying == True:
await self.play_button_clicked(e)
if self.save_button.text == '*Save':
#print('Save is not done.')
await self.speech_save_or_cancel()
else:
await self.pick_speech_file()
# Opens a dialog if change is not saved.
async def speech_save_or_cancel(self):
self.page.dialog = self.speech_save_or_cancel_dialog
self.speech_save_or_cancel_dialog.open = True
self.page.update()
# Alert dialog that opens if subtitle was edited but not saved when Open Speech File button is clicked.
self.speech_save_or_cancel_dialog = ft.AlertDialog(
modal=True,
title=ft.Text('Change not saved.'),
content=ft.Text('Do you want to discard the change?'),
actions=[
#ft.TextButton('Save', on_click=self.save_then_open, tooltip='Save then open another file.'),
ft.TextButton('Open without save', on_click=self.open_speech_without_save, tooltip='Change will be lost.'),
ft.TextButton('Cancel', on_click=self.close_speech_save_or_cancel_dialog),
]
)
未保存の変更があるときに開くダイアログです。セーブせずにファイルを開く Open without save と、キャンセルしてダイアログを閉じる Cancel ボタンが配置されています。ここからセーブもできるようにしたかったのですがうまくいかず、Save ボタンはコメントで残っています。
# Saves as .txt file.
async def save_as_txt(self, save_file_name):
with open(save_file_name, 'w') as txt:
for i in self.subtitles:
for j in range(len(i)):
if j % 4 == 3:
txt.write('%s\n' % i[j])
notification = f'Subtitle saved as a TXT file: {os.path.basename(save_file_name)}'
await self.open_notification_bar(notification)
self.update()
# Exports current open SRT file as another SRT file.
async def export_as_srt(self, e):
if os.path.splitext(self.text_file)[1] == '.srt':
suggested_file_name = os.path.basename(self.text_file).split('.', 1)[0]+'_'+datetime.now().strftime("%Y%m%d%H%M")+'.srt'
self.export_as_srt_dialog.save_file(
dialog_title='Export as an SRT file',
allowed_extensions=['srt'],
file_name = suggested_file_name,
file_type=ft.FilePickerFileType.CUSTOM,
)
# Checks result of Export as SRT File Picker and passes absolute path to self.save_as_srt if exists.
async def export_as_srt_result(self, e: ft.FilePicker.result):
if e.path:
await self.save_as_srt(e.path)
TXT で書き出すメソッドのコードを開く
# Exports current open text file as a TXT file.
async def export_as_txt(self, e):
if os.path.exists(os.path.splitext(self.text_file)[0]+'.txt'):
suggested_file_name = os.path.basename(self.text_file).split('.', 1)[0]+'_'+datetime.now().strftime("%Y%m%d%H%M")+'.txt'
else:
suggested_file_name = os.path.basename(self.text_file).split('.', 1)[0]+'.txt'
self.export_as_txt_dialog.save_file(
dialog_title='Export as a TXT file',
allowed_extensions=['txt'],
file_name = suggested_file_name,
file_type=ft.FilePickerFileType.CUSTOM,
)
# Checks result of Export as TXT File Picker and passes absolute path to self.save_as_txt if exists.
async def export_as_txt_result(self, e: ft.FilePicker.result):
if e.path:
await self.save_as_txt(e.path)
# Notification bar control at the bottom
self.notification_bar=ft.SnackBar(
content=ft.Text('Speech + Subtitle Player'),
duration=2000,
bgcolor=ft.colors.BLUE_GREY_700,
)
# Opens notification bar with given text. If type is 'error', shows message longer with caution color.
async def open_notification_bar(self, notification, type='normal'):
if type == 'normal':
self.notification_bar.content=ft.Text(notification, color=ft.colors.LIGHT_BLUE_ACCENT_400)
self.notification_bar.bgcolor=ft.colors.BLUE_GREY_700
elif type == 'error':
self.notification_bar.content=ft.Text(notification, color=ft.colors.RED)
self.notification_bar.bgcolor=ft.colors.YELLOW
self.notification_bar.duration=4000
self.notification_bar.open=True
self.notification_bar.update()
# === BUILD METHOD ===
def build(self):
# Start time button
self.display_start_time = ft.TextButton(text=f"{ms_to_hhmmssnnn(int(self.start_time))}",
# Disable jump button if loaded text is TXT, no timestamp.
disabled=(self.start_time==201355555),
# When enabled, jump to the key when clicked.
key=self.index,
width=130,
on_click=self.jump_clicked,)
# Subtitle text button in display view. Click to edit.
self.display_text= ft.TextButton(text=f"{self.text}",
on_click=self.edit_clicked,
tooltip='Click to edit')
# Placeholder of subtitle text button in edit view.
self.edit_text = ft.TextField(expand=1)
# Put controls together. Left item is the key=index.
self.display_view = ft.Row(
alignment=ft.MainAxisAlignment.START,
controls=[
ft.Text(value=self.index, width=30),
self.display_start_time,
self.display_text,
]
)
# Change tool tip of start time button which is only clickable for SRT.
if self.start_time==201355555:
self.display_start_time.tooltip='Jump not available'
else:
self.display_start_time.tooltip='Click to jump here'
# Subtitle edit view visible when clicked.
self.edit_view = ft.Row(
visible=False,
#alignment=ft.MainAxisAlignment.SPACE_BETWEEN,
#vertical_alignment=ft.CrossAxisAlignment.CENTER,
controls=[
self.edit_text,
ft.IconButton(
icon=ft.icons.DONE_OUTLINE_OUTLINED,
tooltip='Update Text',
on_click=self.save_clicked,
),
ft.IconButton(
icon=ft.icons.CANCEL_OUTLINED,
tooltip='Close wihout change',
on_click=self.cancel_clicked,
)
]
)
return ft.Column(controls=[self.display_view, self.edit_view])
Flet は割と簡単にモダンなデザインでアプリが作れるので、そこが大きな魅力です。GUI の構成要素となるボタンやテキストはもちろん、音声ファイルの処理やスライダー、通知やダイヤログなど、あらかた必要なものはそろっており、細かいところは気にせずに配置していくだけで、とりあえずアプリっぽいものが作れてしまいます。持っていないので想像でしかないのですが、質の良い 3D プリンタを手に入れたのと近い気がしています。3D モデルデータが実物になるように、Python のコードが触れるようになるという感覚です。
オフィシャルのドキュメントは充実しており、うまく理解できれば自分のアプリに利用できます。基本的に全ての機能はウェブブラウザで動く設計なので、実際に触って試せる Live example も多数用意されていて、いじりながら自分のアプリに使う部品を探す作業も楽しいです。このギャラリーに行けば、おそらくほとんどのコントロールや機能のサンプルを試し、GitHub で実際のコードの確認が行えるので、すごく助かります。