end0tknr's kipple - web写経開発

太宰府天満宮の狛犬って、妙にカワイイ

visual studio 2022 + c# による autocad 2026 plugin開発環境の準備

autolisp でも、ある程度の自動化は可能ですが、 ユーザがクリックするボタンを持つツールパレットまでは作成できない為、 今回、visual studio 2022 + c# による autocad 2026 plugin開発環境を準備

1 Visual Studio 2022 & Emacs Emulation の install

現時点で最新の vs2026ですが、vs環境に emacs keybind を提供する Emacs Emulation を利用したい為、若干、古い vs2022を次のurlより、 VisualStudio 2022 proをダウンロード & インストール。

尚、vsの古いversionoをダウンロードする場合、 msのアカウントでログインする必要がありました

vsを起動後、メニューバーの拡張機能 → 拡張機能の管理 で画面遷移し、 Emacs Emulation を インストール。

更にメニューバーのツール → オプション で、 キーボードマップスキームを Emacsに設定し、完了

2 sample addon の 作成

2.1 プロジェクト新規作成

vsを起動後、プロジェクト新規作成から、 c# クラスライブラリ、.NET 8.0 を選択し、作成

2.2 autocad 2026 の dllへの参照追加

上記より、以下の3つのdllを参照追加

C:\Program Files\Autodesk\AutoCAD 2026
  accoremgd.dll(コマンド・ドキュメント関連)
  acdbmgd.dll(データベース/エンティティ)
  acmgd.dll(アプリケーション・エディタ)

2.3 .csprojの編集

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net8.0-windows</TargetFramework>
    <PlatformTarget>x64</PlatformTarget>
    <ImplicitUsings>disable</ImplicitUsings>
    <Nullable>disable</Nullable>
    <UseWindowsForms>true</UseWindowsForms>       
  </PropertyGroup>
  <ItemGroup>
    <Reference Include="accoremgd">
      <HintPath>C:\Program Files\Autodesk\AutoCAD 2026\accoremgd.dll</HintPath>
      <Private>False</Private>
    </Reference>
    <Reference Include="acdbmgd">
      <HintPath>C:\Program Files\Autodesk\AutoCAD 2026\acdbmgd.dll</HintPath>
      <Private>False</Private>
    </Reference>
    <Reference Include="acmgd">
      <HintPath>C:\Program Files\Autodesk\AutoCAD 2026\acmgd.dll</HintPath>
      <Private>False</Private>
    </Reference>
    <Reference Include="AdWindows">
      <HintPath>C:\Program Files\Autodesk\AutoCAD 2026\AdWindows.dll</HintPath>
      <Private>False</Private>
    </Reference>    
  </ItemGroup>
</Project>

2.4 .csの編集

using Autodesk.AutoCAD.Runtime;
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Windows;        // PaletteSet
using AcadApp = Autodesk.AutoCAD.ApplicationServices.Application;
using System;
using System.Drawing;
using System.Windows.Forms;

[assembly: ExtensionApplication(typeof(ClassLibrary2.MyPlugin))]
[assembly: CommandClass(typeof(ClassLibrary2.MyPlugin))]

namespace ClassLibrary2
{
    public class MyPlugin : IExtensionApplication
    {
        // PaletteSet は使い回すため static で保持
        private static PaletteSet _ps;

        public void Initialize() { }
        public void Terminate() { }

        // このコマンドでパレットを表示する
        [CommandMethod("SHOWPALETTE")]
        public void ShowPalette()
        {
            if (_ps == null)
            {
                _ps = new PaletteSet("My Tools",
                    new Guid("D7A4C2E1-6F3B-4A9C-8E1D-2B5F7A9C3E4D"));
                _ps.Style = PaletteSetStyles.ShowAutoHideButton
                          | PaletteSetStyles.ShowCloseButton;
                _ps.MinimumSize = new Size(200, 150);

                // 自作のコントロール(ボタンを載せた面)を追加
                MyPaletteControl control = new MyPaletteControl();
                _ps.Add("Panel1", control);
            }
            _ps.Visible = true;
        }

        // ボタンから呼ばれる実処理
        public static void SayHello()
        {
            Document doc = AcadApp.DocumentManager.MdiActiveDocument;
            doc.Editor.WriteMessage("\nHello .Net plugin !");
            MessageBox.Show("Hello .Net plugin !", "My Plugin");
        }
    }

    // パレットの中身:ボタンを1つ載せた UserControl
    public class MyPaletteControl : UserControl
    {
        public MyPaletteControl()
        {
            Button button = new Button
            {
                Text = "Hello",
                Location = new Point(20, 20),
                Size = new Size(120, 40)
            };
            button.Click += (sender, e) => MyPlugin.SayHello();

            this.Controls.Add(button);
            this.BackColor = SystemColors.Control;
        }
    }
}

addonの build & run

vsのメニューバーから、ビルド → ソリューションのビルドを選択すると、 ClassLibrary?/ClassLibrary?/bin/Debug/net8.0-windows にdllが作成。

autocad 2026を起動し、NETLOADコマンドにより、このdllを読込み、 SHOWPALETTE コマンドを実行すると、ツールパレットが表示。

「Hello」をクリックすると、メッセージボックスと、コンソールに Helloが表示され、完了

graphragの index処理で作成されるparquetファイルと lancedb ファイル

先程のentryに関連するメモ

parquet

documents.parquet - 入力ドキュメント

カラム 意味
id document id
human_readable_id
title 通常は入力file名
text ドキュメントの全文
text_unit_ids この文書が分割されてできたchunk id 一覧
creation_date GraphRAG が処理した日時
raw_data meta data。csv入力時の他カラムや、metadataで指定した付加情報I

text_units.parquet - documents.parquet から分割の chunk

documents を分割した chunk

カラム 意味
id document id
human_readable_id
text このchunkのtext
n_tokens text のトークン数
document_id 親document。1chunkが複数文書に跨る可能性がある為、list型
entity_ids このchunkから抽出されたentity id一覧
relationship_ids このchunkから抽出されたrelation id 一覧
covariate_ids このchunkから抽出された共変量(covariate / claim)id 一覧

covariate_idsのcovariate とは、entityと relationshipだけでは表現しきれない 「あるentityについての具体的な事実・主張」。

entities.parquet - 抽出されたentity (node)

カラム 意味
id document id
human_readable_id
name entityの名前。LLM が原文から抽出・正規化した表記
type entityの種別。抽出promptの entity_typesで指定のcategoryに分類
description entityの説明文
text_unit_ids entityが抽出された元chunk id 一覧
frequency このentityが何個のchunkに出現したか。頻度
degree このnodeに接続している relationship(エッジ)の本数

relationships.parquet

カラム 意味
id document id
human_readable_id
source 始点entity 例:国税庁
target 終点entity 例:E-TAX ソフト
description 関係の説明文 例:国税庁が開発・運用する電子申告ソフト
weight 関係の強さ。同じ関係が多く繰り返し抽出されるほど高い
combined_degree source と target のdegreeの合計。ハブ的な関係ほど大きい
text_unit_ids この関係の根拠となった元chunk id

communities.parquet

Leiden法というコミュニティ検出らしいが、Leiden法を理解していません

カラム 意味
id document id
human_readable_id
community community no。community_reports.parquet と紐づくキー
level 階層の深さ。数字が大きいほど細分化されたsub community
parent 親community no。最上位は -1(親なし)
children 子community no。このcommunityがさらに分割された先
title communityの名称
entity_ids このcommunityに属するentity id一覧
relationship_ids このcommunityに含まれる relationship一覧
text_unit_ids 構成要素の根拠となったtext chunk一覧
period このcommunityが作られた日(index実行日)
size communityの規模。所属entity数。大きいほど主要なテーマ

community_reports.parquet

各communityをLLMが要約・分析したレポート

カラム 意味
id document id
human_readable_id
community communities.parquet の community と紐づくkey
level 対象communityの階層の深さ
parent 親community no。最上位は -1(親なし)
children 子community no。このcommunityがさらに分割された先
title communityの名称
summary communityの要約
full_content reportの本文全体
rank このcommunityの重要度
rating_explanation 重要度を評価した理由
findings 詳細な発見事項のlist。各要素が小見出し根拠付き説明を持つ
full_content_json 上記reportのjson形式
period* report生成日。index実行日
size communityの規模。所属entity数

parquet の内容の例

import glob
import pandas as pd

def main():

    pd.set_option("display.max_columns", None)   # 全カラム表示
    pd.set_option("display.width", None)
    pd.set_option("display.max_colwidth", 50)    # 長文セルを省略

    f = "output/"+ "documents.parquet"
    print("####" + f +"####")
    df = pd.read_parquet(f)
    cols = ["title"]
    print_records(df, cols, n=5)

    f = "output/"+ "entities.parquet"
    print("####" + f +"####")
    df = pd.read_parquet(f)
    cols = ["title","type","description","frequency","degree"]
    print_records(df, cols, n=5)

    f = "output/"+ "relationships.parquet"
    print("####" + f +"####")
    df = pd.read_parquet(f)
    cols = ["source", "target", "description", "weight", "combined_degree"]
    print_records(df, cols, n=5)


def print_records(df, cols, n=10):
    max_len = 30
    
    for i, record in enumerate(df[cols].head(n).to_dict("records"), start=1):
        print(f"--- Row {i} ---")
        for key, value in record.items():
            if type(value) is str and len( value ) > max_len:
                value = value[:max_len] + "..."
            print(f"{key} : {value}")
        print()

if __name__ == "__main__":
    main()
C:\Users\end0t\tmp\GraphRag>..\..\python3.12\python.exe view_parquet.py
####output/documents.parquet####
--- Row 1 ---
title : e-Tax_man-001.md

--- Row 2 ---
title : e-Tax_man-002.md

--- Row 3 ---
title : e-Tax_man-003.md

--- Row 4 ---
title : e-Tax_man-004.md

--- Row 5 ---
title : e-Tax_man-005.md

####output/entities.parquet####
--- Row 1 ---
title : E-TAXソフト
type : SOFTWARE
description : **E-TAXソフト**は、国税庁と連携して税務処理を全面的...
frequency : 101
degree : 233

--- Row 2 ---
title : 2026年5月版
type : IDENTIFIER
description : e-Taxソフト操作マニュアルのバージョン識別子で、2026...
frequency : 2
degree : 1

--- Row 3 ---
title : 国税庁
type : SYSTEM
description : **国税庁**は、日本の税務行政を担う中心的な政府機関であり...
frequency : 8
degree : 12

--- Row 4 ---
title : WINDOWS
type : SYSTEM
description : Microsoft社が開発したオペレーティングシステム「WI...
frequency : 2
degree : 3

--- Row 5 ---
title : PENTIUM
type : SYSTEM
description : Intel社が開発したプロセッサで、コンピュータシステムのハ...
frequency : 1
degree : 1

####output/relationships.parquet####
--- Row 1 ---
source : E-TAXソフト
target : 2026年5月版
description : e-Taxソフト操作マニュアルのバージョンとして関連付けられ...
weight : 2.0
combined_degree : 234

--- Row 2 ---
source : 国税庁
target : E-TAXソフト
description : 国税庁がe-Taxソフトを提供・管理するシステム
weight : 2.0
combined_degree : 245

--- Row 3 ---
source : E-TAXソフト
target : 著作権
description : e-Taxソフトの利用に際して著作権に関する規定が適用される
weight : 1.0
combined_degree : 235

--- Row 4 ---
source : ADOBE ACROBAT READER
target : 著作権
description : Adobe Acrobat Readerは著作権保護されたド...
weight : 1.0
combined_degree : 4

--- Row 5 ---
source : WINDOWS
target : E-TAXソフト
description : e-Taxソフトは、Windowsシステム上で動作する税務処...
weight : 2.0
combined_degree : 236

lancedb

community_full_content.lance , entity_description.lance , text_unit_text.lance

意味的な類似検索に使用するベクトルストア

テーブル 主な利用先
text_unit_text Local Search / Basic Search(質問に近い元chunkを探す)
entity_description Local Search(質問に関連するentityを特定する)
community_full_content DRIFT Search (質問に近いcommunity要約を探す)

各テーブルの主要カラム:

カラム 内容
id 元データのID(対応するparquetの行を指す)
vector bge-m3が生成する場合、1024次元
create_date 生成日時

community_full_content.lance , entity_description.lance , text_unit_text.lance の違い

テーブル 埋込み対象 元データ
text_unit_text 元textのchunk本文 text_units.parquet
entity_description entityの説明文 entities.parquet の description
community_full_content community report全文 community_reports.parquetの full_content

graphrag + python for win + ollama + 日本語

GraphRAGを触ったことがなかった為、 graphrag + python for win + ollama + 日本語 の環境を構築。

構築後、graphragへ質問し、日本語での回答を得られました。 しかし、graphragらしい?回答の高精度さまでは感じられませんでした。

また、index作成やqueryからの回答生成までの時間が長く、 実用には難しい気がします。

目次

0 環境

項目 内容
PC thinkpad x1 carbon mem:64GB
GPU geforce etx 4090 mem:24GB
GraphRag python3.12, graphrag 3.1
LLM ollama ver.0.30
MODEL bge-m3(1 GB), qwen3:8b(5 GB)

1 install

C:\Users\end0t> python3.12\python.exe --version
Python 3.12.10
C:\Users\end0t> python3.12\python.exe -m pip install graphrag

標準で graphragの入力できるものは、txt, csv, json のみですので、 markdownを入力する為の graphrag-input や、 markdownへ変換する為の markitdown[all] も installします

C:\Users\end0t> python3.12\python.exe -m pip install graphrag-input
C:\Users\end0t> python3.12\python.exe -m pip install markitdown[all]

2 環境の初期化

graphrag.exe init を実行すると、settings.yamlの設定ファイルや、 入力fileを登録する input dir などが作成されます。 chatや embedding用のmodelの入力を求められますが、 後から settings.yaml で変更できますので、ここではそのままenter

C:\Users\end0t\tmp> mkdir GraphRag
C:\Users\end0t\tmp> cd GraphRag

C:\Users\end0t\tmp\GraphRag>..\..\python3.12\Scripts\graphrag.exe init
Specify the default chat model to use [gpt-4.1]:
Specify the default embedding model to use [text-embedding-3-large]:

実行後の状態は以下

C:\Users\end0t\tmp\GraphRag>tree /F
フォルダー パスの一覧:  ボリューム Windows
ボリューム シリアル番号は 00000055 3418:EFBE です
C:.
│  .env
│  settings.yaml
├─input
└─prompts
        basic_search_system_prompt.txt
        community_report_graph.txt
        community_report_text.txt
        drift_reduce_prompt.txt
        drift_search_system_prompt.txt
        extract_claims.txt
        extract_graph.txt
        global_search_knowledge_system_prompt.txt
        global_search_map_system_prompt.txt
        global_search_reduce_system_prompt.txt
        local_search_system_prompt.txt
        question_gen_system_prompt.txt
        summarize_descriptions.txt

3 promptファイル一覧

prompts dir以下に作成されるpromptファイルは以下

index構築時

知識グラフを作る過程でLLMに渡すプロンプト

file note
community_report_graph.txt comunity要約report生成。entity・関係から community_reports.parquet の要約・f|indings・rank作成
community_report_text.txt comunity reportを元text主体で要約
extract_claims.txt textから主張・事実(claim/covariate)を抽出
extract_graph.txt textからentity(人・組織・用語)と関係を抽出。entities.parquet/relationships.parquet を生成。
summarize_descriptions.txt 同じentity/関係が複数chunkから抽出の際、説明文を1つに要約。entityのdescription列を作成

検索時

Local Search

file note
local_search_system_prompt.txt entity・関係・元textを集め回答を生成。

Global Search

file note
global_search_map_system_prompt.txt Map段階。comunity reportから関連する要点を抽出
global_search_reduce_system_prompt.txt Reduce段階。集めた要点を統合し最終回答を生成
global_search_knowledge_system_prompt.txt model自身の一般知識を混ぜるか制御

DRIFT Search — local と global の中間(段階的に深掘り)

file note
drift_search_system_prompt.txt comunity情報で大枠を掴み、追加質問で局所情報へ
drift_reduce_prompt.txt 掘下げて集めた中間回答を統合し最終回答を生成

Basic Search — グラフを使わない素朴なRAG

file note
basic_search_system_prompt.txt 元textのvector検索のみで回答

その他

file note
question_gen_system_prompt.txt Q&A履歴から、userが次に聞きそうな質問を生成

3 ollama用 modelのdownload

当初、qwen3.6:27b(17 GB)を使用しましたが、thinkpad mem64GB + rtx4090でも graphrag.exe index 実行が固まった為、qwen3:8b を使用

C:\Users\end0t> ollama pull bge-m3:latest
C:\Users\end0t> ollama pull qwen3-embedding:8b

C:\Users\end0t\tmp\GraphRag>ollama ls
NAME                  ID              SIZE      MODIFIED
bge-m3:latest         790764642607    1.2 GB    13 hours ago
qwen3:8b              500a1f067a9f    5.2 GB    5 days ago

4 settings.yaml の編集

編集した場所付近を、ポイントと共に以下に抜粋しています。

ollamaやmodelの指定の他、index作成時やquery実行時に固まった為、 concurrentを低くするなどを設定しています。

### LLM settings ###
concurrent_requests: 2   # 変更 Ollama過負荷を避ける為

completion_models:
  default_completion_model:
    model_provider: openai
    model: qwen3:8b                     # 変更    
    auth_method: api_key
    api_key: ollama                     # 変更
    api_base: http://localhost:11434/v1 # 変更
    retry:
      type: exponential_backoff

embedding_models:
  default_embedding_model:
    model_provider: openai
    model: bge-m3:latest                # 変更
    auth_method: api_key
    api_key: ollama                     # 変更
    api_base: http://localhost:11434/v1 # 変更
    concurrent_requests: 1              # 変更 Ollama過負荷を避ける為
    async_mode: threaded
    retry:
      type: exponential_backoff

### Document processing settings ###

input:
  type: text # [csv, text, json, jsonl]
  file_pattern: ".*\\.md"               # 変更
chunking:
  type: tokens
  size: 1200
  overlap: 100
  encoding_model: o200k_base

# 【中略】

vector_store:
  type: lancedb
  db_uri: output\lancedb
  vector_size: 1024   # 変更 bge-m3 は1024次元の為

# 【中略】

drift_search:
  completion_model_id: default_completion_model
  embedding_model_id: default_embedding_model
  prompt: "prompts/drift_search_system_prompt.txt"
  reduce_prompt: "prompts/drift_search_reduce_prompt.txt"
  concurrency: 2          # 変更 32→2 Ollama過負荷回避
  drift_k_followups: 5    # 変更 20→5 追加質問数を削減
  n_depth: 2              # 変更 3→2 深掘り階層を削減
  primer_folds: 3         # 変更 5→1 初期探索の分割数を削減

5 input用 markdownの準備

e-Taxのマニュアル(約600ページ)をダウンロード。 graphragからの回答の際、出典ページも出力したかった為 ページ毎に分割したpdfを markitdown で mdファイル化し、input dirへ登録。

from markitdown import MarkItDown
import os
import sys
import glob

def main():
    pdf_dir = sys.argv[1]
    md      = MarkItDown()
    
    for pdf_path in glob.glob( pdf_dir +"/*.pdf" ):
        basename = os.path.splitext( os.path.basename(pdf_path) )[0]
        md_path  = "input/%s.md" % ( basename )
        print( basename )
        result = md.convert( pdf_path )
        
        with open( md_path, "w", encoding="utf-8") as f:
            f.write( result.text_content )
            
if __name__ == "__main__":
    main()

6 graphrag.exe prompt-tune などによる promptの修正

defaultのpromptファイルは英語版ですし、qwen3が thinkingモードで動作すると、 graphrag.exe index の処理時間が長くなる為、修正します。

C:\Users\end0t\tmp\GraphRag>..\..\python3.12\Scripts\graphrag.exe prompt-tune --language Japanese

上記を実行すると、複数あるpromptファイルのうち summarize_descriptions.txt extract_graph.txt community_report_graph.txt の内容が更新されましたが、変更後の内容は理解できませんでした。

7 graphrag.exe index 実行

graphrag.exe index を実行することで、input dirにある markdownが処理され output以下に parquetファイルや lanceファイルが作成されます。

尚、このindex作成には、60分以上を要しました。

C:\Users\end0t\tmp\GraphRag>..\..\python3.12\Scripts\graphrag.exe index

上記によるoutput以下への成果物

file or dir note
communities.parquet グラフをクラスタリングしたコミュニティ
community_reports.parquet 各コミュニティのLLM生成要約レポート
context.json 実行統計・状態
documents.parquet 入力ドキュメント
entities.parquet 抽出されたエンティティ(ノード)
lancedb/ 埋め込みベクトルのストア(検索用)
relationships.parquet エンティティ間の関係(エッジ)
stats.json 実行統計・状態
text_units.parquet チャンク化されたテキスト単位

8 graphragに対しての質問

..\..\python3.12\Scripts\graphrag.exe query --method global "このマニュアルで扱われている主な機能を日本語で列挙してください"
### e-Taxシステムの主な機能の列挙

#### 1. **税務処理の中心的プラットフォームの提供**
e-Taxソフトウェアは、税務処理の核となるデジタルツールとして機能し、電子申告・納税、データセキュリティ、システムメンテナンス、規制遵守を統合的に扱います [Data: Reports (5, 35, 77, 32, +more)]. これにより、税務業務の信頼性と効率化が実現されます [Data: Reports (223, 2, 268, 126, +more)].

#### 2. **電子署名とセキュリティ管理**
電子署名技術が税務文書の正当性と改ざん防止を確保するための核心的な技術であり、税務署長の証明やトラストアンカーを基盤にした検証プロセスを通じて税額計算書や国税還付金振込通知書などの法的効力が保証されています [Data: Reports (1, 7, 127)]. SSL/TLS通信や電子証明書を活用したセキュリティ確保が行われています [Data: Reports (54, 15, 244)]. 電子証明書検証、改ざん検知、保存・印刷プロセスが税務情報の信頼性とガバナンスを支えます [Data: Reports (0, 329, 225, 169, 306, +more)].

【省略】
..\..\python3.12\Scripts\graphrag.exe query --method local "電子証明書の登録方法を日本語で教えてください"
電子証明書の登録方法は、日本の税務行政におけるデジタル化の一環として厳格に規定されており、以下のように手順を踏む必要があります。登録プロセスは、利用者自身が税務手続きの信頼性を確保するために不可欠です。

### 電子証明書の登録手順
1. **e-Taxソフトウェアの起動とログイン**
   電子証明書を登録するには、まずe-Taxソフトウェアを起動し、受付システムにログインする必要があります。ログイン時に暗証番号や認証情報を入力し、システムにアクセスします([Data: Sources (88, 599); Relationships (949)])。

2. **電子証明書の選択と登録**
   ログイン後、メニューから「電子証明書を登錤又は更新する」を選択します([Data: Sources (88, 599); Entities (474, 480)])。ここで、登録可能の電子証明書を指定します。選択肢には、ICカード、GビズIDアプリ、または電子証明書ファイルがあります。ICカードを利用する場合、ICカードリーダライタにカードを挿入し、認証を完了する必要があります([Data: Sources (88, 179); Entities (39, 765)])。

【省略】

まず全体像を掴み、追加質問しながら掘り下げる方式

..\..\python3.12\Scripts\graphrag.exe query --method drift "確定申告をe-Taxで行う一連の流れを、準備から提出まで日本語で説明してください"
確定申告をe-Taxで行う際の流れは、準備から提出まで以下の手順で進められます。

### 1. **準備段階**
まず、e-Taxソフトをインストールし、確定申告の開始に必要な「開始届出書」を税務署に提出します。この書類には利用者識別番号や暗証番号が含まれ、電子証明書の認証を確保します([Data: Sources (125, 156, 198)])。同時に、給与明細、退職金証明などの収入証明書を揃え、電子署名を付与する必要があります([Data: Sources (1, 5, 15)])。

### 2. **入力・計算段階**
e-Taxソフトにアクセスし、納税申告書の情報を入力します。税額計算はソフト内蔵の機能で自動的に算出され、税額計算書が生成されます。操作画面では、税務処理の手順や選択肢が明確に表示され、入力ミスを防ぎます([Data: Sources (1, 5, 15)])。数値入力項目(収入、控除額、税額など)は手動で修正可能です。入力後は自動計算機能が働いて、関連する欄に結果が反映されます([Data: Sources (140, 141)])。

### 3. **提出段階**
入力した情報を確認後、e-Taxで税務申告書を提出します。この際、SSL/TLS 256BIT暗号化技術が利用されており、個人情報のセキュリティが確保されます([Data: Sources (1, 5, 15)])。提出後、メッセージボックストに手続きの結果が通知されます。修正が必要な場合、「申告・申請等一覧」画面で選択し、[詳細表示]をクリックして内容を確認し、修正後再送信します([Data: Sources (125, 156, 198)])。


【省略】

ベクトル検索のみ

..\..\python3.12\Scripts\graphrag.exe query --method basic "メールアドレスの登録について日本語で教えてください"
メールアドレスの登録手順について、以下のように説明します。関連するデータは[ID: 107, 108, 109, 588]を参照してください。

---

### メールアドレスの登録手順
1. **登録画面へのアクセス**
   受付システムにログイン後、メニューから「利用者情報登録」→「メールアドレス等登録・変更」を選択します。未ログインの場合は、46ページのログイン手順に従ってください [Data: Sources (107, 108, 109, 588)]。
2. **入力内容の指定**
   - **メインメールアドレス**と**サブメールアドレス**(1番目・2番目)を入力します。確認用のメールアドレスも同時に記載する必要があります。
   - 電子納税証明書の通知を受け取りたい場合は、「宛名の表示を希望する」にチェックを入れ、お知らせメールに表示する氏名や名称を入力します [Data: Sources (107, 108, 109)]。
3. **登録の完了**
   入力後、OKをクリックすると、メールアドレスが受付システムに登録されます。登録確認メールが送信されるため、受信設定を確認してください [Data: Sources (107, 108, 109)]。

---

### 宛名の確認と通知
- **宛名の登録完了**
  宛名の表示を希望した場合、登録したメインメールアドレスに「税務署からのお知らせ【宛名の登録確認】」という案内メールが届きます。メール内のURLにアクセスし、利用者識別番号や暗証番号で認証することで、宛名の登録が完了します [Data: Sources (109, 588)]。

【省略】

autocadの autolispマクロの練習 - その4

BOXをタイルのように敷き詰めました。

オブジェクトスナップONのままでは、タイル同士が隣接してしまうようでした。

(defun TileArray ( w h spaces_h spaces_v
                  / x1 x2 y1 y2 ret_ents
                  old_osmode old_snapmode)
  
  (setq old_osmode   (getvar "OSMODE")
        old_snapmode (getvar "SNAPMODE"))
  (setvar "OSMODE"   0) ; スナップ OFF
  (setvar "SNAPMODE" 0)

  (setq y1 0
        y2 h)
  (setq ret_ents nil)

  (foreach space_v spaces_v
    (setq x1 0
          x2 w)
    (foreach space_h spaces_h
       (command "_.BOX" (list x1 y1 0) (list x2 y2 10))
       (setq ret_ents (append ret_ents (list (entlast))))
       (setq x1 (+ x2 space_h))
       (setq x2 (+ x1 w))
    )
    (setq y1 (+ y2 space_v)
          y2 (+ y1 h))
  )

  ;; スナップを元に戻す
  (setvar "OSMODE"   old_osmode)
  (setvar "SNAPMODE" old_snapmode)
  ret_ents
)


(defun c:CreateTiles ( / tiles )
  (setq tiles
    (TileArray 104 104
               '(8.0 9.0 8.0 9.0 8.0 9.0 8.0)
               '(8.0 8.0 8.0 8.0 8.0)))

  (princ))

windows batの練習 - UAC無効化、ドメイン参加、ユーザ追加、

windows batの練習 - UAC無効化、ドメイン参加、ユーザ追加、

UAC無効化

@echo off
rem インストール前の準備

echo インストール前の準備を開始します。
echo;
echo 【処理内容】
echo ・UAC無効化
echo ※設定後、再起動します。
echo;

openfiles > NUL 2>&1 
if NOT %ERRORLEVEL% EQU 0 (
  echo 管理者権限で実行してください
  pause
  goto EXIT
)

set REGKEY=HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System
set REGNAME=EnableLUA
FOR /F "TOKENS=1,2,*" %%I IN ('REG QUERY "%REGKEY%" /v "%REGNAME%"') DO IF "%%I"=="%REGNAME%" SET VAL1=%%K
if "%VAL1%" EQU "0x0" (
  echo 既にUACが無効になっているので、処理不要です。
  echo 処理を中断して終了します。
  echo;
  pause
  goto EXIT
)


echo UACを無効化します。
reg add HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System /v EnableLUA /t REG_DWORD /d 0 /f
echo UACを無効化しました。
echo;

echo 10秒後再起動します。

shutdown /r /t 10
pause
:EXIT

ドメイン参加

@echo off
cls


set domain_name=naisho.local
set user_name=naisho_admin_id
set user_password=aisho_admin_pw

echo %COMPUTERNAME%%domain_name%ドメインに参加させます。

:pause >NUL

echo %domain_name%ドメインに参加中...
powershell -Command "$user=\"%user_name%\"; $password=ConvertTo-SecureString -AsPlainText %user_password% -Force; $credential=New-Object system.Management.Automation.PSCredential $user,$password; Add-Computer -DomainName %domain_name% -Credential $credential -Force"

echo 設定を反映するため、PCを10秒後に再起動します......
shutdown /r /t 10
pause

exit

ユーザ追加

@echo off

rem clsで画面clear
cls

rem  遅延環境変数の展開を有効化
setlocal EnableDelayedExpansion

:: ==================================================
:: 登録ユーザー一覧
:: 形式:ユーザー名,パスワード,フルネーム
:: ==================================================
set USERS[1]=naisho-id1,naisho-pw1,コンピュータ管理者ID
set USERS[2]=naisho-id2,naisho-pw2,IPアドレス設定ID
::set USERS[3]=user3,pass3,管理者3   ← 追加はここ

:: ユーザー数
set MAX=2

echo ==== 管理者ユーザー一括登録 開始 ====
echo.

for /L %%i in (1,1,%MAX%) do (

    for /F "tokens=1-3 delims=," %%A in ("!USERS[%%i]!") do (
        set USERNAME=%%A
        set PASSWORD=%%B
        set FULLNAME=%%C
    )

    echo ----------------------------------
    echo 対象ユーザー:!USERNAME!

    set R1=NG
    set R2=NG
    set R3=NG
    set R4=NG
    set R5=NG

    echo 1. ユーザー作成
    net user "!USERNAME!" "!PASSWORD!" /add >nul 2>&1
    if errorlevel 1 (
        echo → 既に存在 or 失敗
    ) else (
        set R1=OK
    )

    echo 2. Administrators 追加
    net localgroup Administrators "!USERNAME!" /add >nul 2>&1
    if errorlevel 1 (
        echo → 失敗
    ) else (
        set R2=OK
    )

    echo 3. Users から削除
    net localgroup Users "!USERNAME!" /delete >nul 2>&1
    if errorlevel 1 (
        echo → 失敗(既に未所属の可能性)
    ) else (
        set R3=OK
    )

    echo 4. フルネーム設定
    net user "!USERNAME!" /fullname:"!FULLNAME!" >nul 2>&1
    if errorlevel 1 (
        echo → 失敗
    ) else (
        set R4=OK
    )

    echo 5. パスワード無期限
    powershell -NoProfile -ExecutionPolicy Bypass ^
      "Set-LocalUser -Name '!USERNAME!' -PasswordNeverExpires $true" >nul 2>&1
    if errorlevel 1 (
        echo → 失敗
    ) else (
        set R5=OK
    )

    echo 結果:
    echo  作成        → !R1!
    echo  管理者追加  → !R2!
    echo  Users削除   → !R3!
    echo  フルネーム  → !R4!
    echo  PW無期限    → !R5!
    echo.
)

echo ==== 全ユーザー処理完了 ====
pause
endlocal

その他 いろいろ

@echo off
cls
setlocal enabledelayedexpansion

:: ===============================
:: タイトル定義
:: ===============================
set R1_title=1. [Administrators] に Domain Users を登録
set R2_title=2. [Users] から Domain Users を削除
set R3_title=3. ネットワーク探索を有効
set R4_title=4. お気に入りコピー
set R5_title=5. 無線LAN登録
set R6_title=6. カバーを閉じても何もしない
set R7_title=7. BitLocker ポリシー設定
set R8_title=8. 最後のサインインユーザー非表示

:: ===============================
:: 判定初期値
:: ===============================
set R1=OK
set R2=OK
set R3=OK
set R4=OK
set R5=OK
set R6=OK
set R7=OK
set R8=OK

set DOMAINNAME=Domain Users

echo ==== 実行開始 ====
echo.

:: --------------------------------------------------
:: 1. Administrators に Domain Users を登録
:: --------------------------------------------------
echo %R1_title%

:: すでに Administrators に含まれているか確認
net localgroup administrators | findstr /i /c:"%DOMAINNAME%" >nul

if not errorlevel 1 (
    echo → 既に登録済み(OK)
) else (
    :: 未登録なので追加
    net localgroup administrators "%DOMAINNAME%" /add
    if errorlevel 1 (
        set R1=NG
        echo → 登録失敗(NG)
    ) else (
        echo → 登録成功(OK)
    )
)

echo.

:: --------------------------------------------------
:: 2. Users から Domain Users を削除
:: --------------------------------------------------

echo %R2_title%

net localgroup Users | findstr /i /C:"%DOMAINNAME%" >nul
if %errorlevel%==0 (
    :: 存在する → 削除
    net localgroup Users "%DOMAINNAME%" /delete >nul 2>&1
    if errorlevel 1 (
        set R2=NG
        echo → 削除失敗(NG)
    ) else (
        echo → 削除成功(OK)
    )
) else (
    :: 存在しない → OK
    echo → 登録されていません(OK)
)

echo.


:: --------------------------------------------------
:: 3. ドメインネットワーク探索有効
:: --------------------------------------------------
echo %R3_title%
powershell -NoProfile -ExecutionPolicy Bypass ^
 "$ProgressPreference='SilentlyContinue'; Get-NetFirewallRule -DisplayGroup 'ネットワーク探索' | Set-NetFirewallRule -Profile Domain -Enabled True" >nul 2>&1

powershell -NoProfile -ExecutionPolicy Bypass ^
 "$ProgressPreference='SilentlyContinue'; Get-NetFirewallRule -DisplayGroup 'ファイルとプリンターの共有' | Set-NetFirewallRule -Profile Domain -Enabled True" >nul 2>&1

if errorlevel 1 (
    set R3=NG
    echo → NG
) else (
    echo → OK
)
echo.

:: --------------------------------------------------
:: 4. お気に入りコピー
:: --------------------------------------------------
echo %R4_title%

xcopy "%~dp0005-Favorites\Favorites" "C:\Users\Default\Favorites" /s /e /i /q /y >nul
if errorlevel 1 set R4=NG

xcopy "%~dp0005-Favorites\Favorites" "%USERPROFILE%\Favorites" /s /e /i /q /y >nul
if errorlevel 1 set R4=NG

:xcopy "%~dp0005-Favorites\Bookmarks" "C:\Users\Default\AppData\Local\Microsoft\Edge\User Data\Default" /s /e /i /q /y >nul
:if errorlevel 1 set R4=NG

echo%R4%
echo.

:: --------------------------------------------------
:: 5. 無線LAN登録
:: --------------------------------------------------
echo %R5_title%

set WLAN_OK=0
for %%f in ("%~dp0906-無線LAN\*.xml") do (
    set WLAN_OK=1
    netsh wlan add profile filename="%%f" user=all >nul 2>&1
    if errorlevel 1 set R5=NG
)

if %WLAN_OK%==0 set R5=NG
echo%R5%
echo.

:: --------------------------------------------------
:: 6. カバーを閉じても何もしない
:: --------------------------------------------------
echo %R6_title%

for /f "tokens=3" %%a in ('powercfg /getactivescheme') do set SCHEME=%%a

powercfg /setacvalueindex %SCHEME% SUB_BUTTONS LIDACTION 0 >nul
if errorlevel 1 set R6=NG

powercfg /setactive %SCHEME% >nul
if errorlevel 1 set R6=NG

echo%R6%
echo.

:: --------------------------------------------------
:: 7. BitLocker ポリシー設定
:: --------------------------------------------------
echo %R7_title%

"%~dp0013-GPO\LGPO.exe" /m "%~dp0013-GPO\registry.pol" >nul 2>&1 || set R7=NG
gpupdate /force >nul 2>&1

echo%R7%
echo.

:: --------------------------------------------------
:: 8. 最後のサインインユーザー非表示
:: --------------------------------------------------
echo %R8_title%

reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System" ^
 /v DontDisplayLastUserName /t REG_DWORD /d 1 /f >nul

if errorlevel 1 set R8=NG
echo%R8%
echo.

:: --------------------------------------------------
:: 最終結果
:: --------------------------------------------------
echo ==============================
echo ■ 最終結果
echo %R1_title%%R1%
echo %R2_title%%R2%
echo %R3_title%%R3%
echo %R4_title%%R4%
echo %R5_title%%R5%
echo %R6_title%%R6%
echo %R7_title%%R7%
echo %R8_title%%R8%
echo ==============================

if "%R1%%R2%%R3%%R4%%R5%%R6%%R7%%R8%"=="OKOKOKOKOKOKOKOK" (
    echo ★ すべて成功しました
) else (
    echo ★ 失敗した処理があります
)

pause
endlocal

windowsのコマンドラインで pc再起動は shutdown -r -t 5

DOS> shutdown -r -t 5
DOS> shutdown /?
使用法: shutdown  [/i | /l | /s | /sg | /r | /g | /a | /p | /h | /e | /o] [/hybrid] [/soft] [/fw] [/f]
    [/m \\コンピューター][/t xxx][/d [p|u:]xx:yy [/c "コメント"]]

    引数なし   ヘルプを表示します。「/?」と入力するのと同じです。
    /?         ヘルプを表示します。オプションを入力しないのと同じです。
    /i         グラフィック ユーザー インターフェイス (GUI) を表示します
               このオプションは最初に指定する必要があります。
    /l         ログオフします。/m または /d オプションとは併用できません。
    /s         コンピューターをシャットダウンします。
    /sg        コンピューターをシャットダウンします。次回の起動時、自動再起動サインオンが
               有効になっている場合は、前回の対話ユーザー自動的にサインインしてロックします。
               サインイン後、登録されているアプリケーションを再起動します。
    /r         コンピューターを完全にシャットダウンして再起動します。
    /g         コンピューターを完全にシャットダウンして再起動します。システムの再起動後、
               自動再起動サインオンが有効になっている場合は、
               前回の対話ユーザーで自動的にサインインしてロックします。
               サインイン後、登録されているアプリケーションを再起動します。
    /a         システムのシャットダウンを中止します。
               これは、タイムアウト期間にのみ使用できます。
               /fw と共に使用して、保留されているファームウェアでの起動をすべてクリアします。
    /p         タイムアウトまたは警告なしでローカル コンピューターの電源を切ります。
               /d および /f オプションと併用できます。
    /h         ローカル コンピューターを休止状態にします。
               /f オプションと併用できます。
    /hybrid    コンピューターのシャットダウンを実行し、高速スタートアップの準備をします。
               /s オプションと併用してください。
    /fw        シャットダウン オプションと共に使用し、次回は以下で起動します
               ファームウェアのユーザー インターフェイス。
    /e         コンピューターの予期しないシャットダウンの理由を記録します。
    /o       詳細ブート オプション メニューに移動し、コンピューターを再起動します。
               /r オプションと併用してください。
    /m \\コンピューター 対象となるコンピューターを指定します。
    /t xxx     シャットダウンまでのタイムアウト期間を xxx 秒に設定します。
               有効範囲は 0 から 315360000 (10 年) までで、既定値は 30 です。
               タイムアウト期間が 0 より大きい場合、/f パラメーターが
               暗黙的に指定されます。
    /c "コメント" 再起動またはシャットダウンの理由にコメントを付けます。
               最大 512 文字まで入力できます。
    /f         ユーザーに警告せずに実行中のアプリケーションを強制終了します。
               0 より大きい値が
 の場合、/f パラメーターが暗黙指定されます               /t パラメーターに指定されます。
    /d [p|u:]xx:yy  再起動またはシャットダウンの理由を指定します。
               p は再起動またはシャットダウンが計画されていることを示します。
               u は理由がユーザーにより定義されることを示します。
               p と u のどちらも指定されていない場合は、再起動またはシャットダウンが
               計画されていません。
               xx は主因の番号です (256 未満の正の整数)。
               yy は副因の番号です (65536 未満の正の整数)。

理由:
(E = 予期していた理由 U = 予期していない理由 P = 計画済み, C = ユーザー定義)
種類    主因    副因    タイトル

 U      0       0       その他 (計画外)
E       0       0       その他 (計画外)
E P     0       0       その他 (計画済)
 U      0       5       その他の障害: システム応答なし
E       1       1       ハードウェア: メンテナンス (計画外)
E P     1       1       ハードウェア: メンテナンス (計画済)
E       1       2       ハードウェア: インストール (計画外)
E P     1       2       ハードウェア: インストール (計画済)
E       2       2       オペレーティング システム: 回復 (計画外)
E P     2       2       オペレーティング システム: 回復 (計画済)
  P     2       3       オペレーティング システム: アップグレード (計画済)
E       2       4       オペレーティング システム: 再構成 (計画外)
E P     2       4       オペレーティング システム: 再構成 (計画済)
  P     2       16      オペレーティング システム: Service pack (計画済)
        2       17      オペレーティング システム: ホットフィックス (計画外)
  P     2       17      オペレーティング システム: ホットフィックス (計画済)
        2       18      オペレーティング システム: セキュリティ フィックス (計画外)
  P     2       18      オペレーティング システム: セキュリティ フィックス (計画済)
E       4       1       アプリケーション: メンテナンス (計画外)
E P     4       1       アプリケーション: メンテナンス (計画済)
E P     4       2       アプリケーション: インストール (計画済)
E       4       5       アプリケーション: 応答なし
E       4       6       アプリケーション: 不安定
 U      5       15      システム障害: STOP エラー
 U      5       19      セキュリティの問題 (計画外)
E       5       19      セキュリティの問題 (計画外)
E P     5       19      セキュリティの問題 (計画済)
E       5       20      ネットワーク接続の損失 (計画外)
 U      6       11      電源障害: コードが抜けました
 U      6       12      電源障害: 環境
  P     7       0       レガシ API シャットダウン

javascript + perl から voicevox web api を 用い tts音声合成

voicevox for win は、無料で利用できるにも関わらず、 高品質な音声合成アプリです。

しかも web api経由で利用できますので、 javascript + perl から voicevox web api を 用い tts音声合成してみました。

全体構成

┌────┐  ┌────┐  ┌────┐  ┌──────┐
│html+js ├→│  iis   ├→│perl cgi├→│voicevox api│
└────┘  └────┘  └────┘  └──────┘

画面構成

html

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>VOICEVOX 音声合成</title>
  <style>
    body {font-family: "Helvetica Neue", Arial,"Hiragino Kaku Gothic ProN",
                       "Hiragino Sans", Meiryo, sans-serif;
          color: #555;}
    a:visited { color: #00f; }
    h1 {margin:5px 0; font-size:1.5em; font-family:"HG丸ゴシックM-PRO";
        display: flex; align-items: center;}
    h1 img { width:32px; height:32px; margin-right:5px; }
    
    h2 {font-size:1.2em;
        font-family:"HG丸ゴシックM-PRO";
        border-left: solid 8px #ABD4AD;
        clear: both;
        padding: 2px;}
    #toast_msg      {color  : #ff4500;}
    #toast_msg.hide {display: none;   }
  
table{border-collapse:collapse;table-layout:fixed;width:100%;}
td,th{border:1px solid #ccc;padding:2px 4px;font-size:0.9em;height:24px;}
tr{height:24px;}
td.name{max-width:300px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
button{cursor:pointer;padding:2px 8px}
.phrase{display:inline-block;margin:2px;padding:2px;border:1px solid #999;background:#fff;}
.mora{display:inline-block;margin:2px;padding:1px;cursor:pointer;user-select:none;font-size:0.85em;}
.mora.accent{background:#ffeb3b;font-weight:bold;}
.mora:hover{background:#e0e0e0;}
  </style>
  <link rel="icon" type="image/png" sizes="16x16" href="voicevox_favicon.png">
</head>
<body>
  <h1>
    <img src="voicevox_icon.jpg"/>
    VOICEVOX - 音声合成
  </h1>
  <div>
    音声合成ソフトの
    <a href="https://voicevox.hiroshiba.jp/" target="_blank">VOICEVOX</a>
    に ブラウザ用画面を設け、複数名による同時利用を可能にしました。<br/>
    <span style="color:#f00"></span>
    <a href="https://voicevox.hiroshiba.jp/term/" target="_blank">VOICEVOX</a>
    の規約に従い、作成した資料には「VOICEVOX:波音リツ」の記載をお願いします
  </div>
  <div id="login_form" style="width:800px; text-align:right;">
    <input type="text"     id="uid"
           autocomplete="off" placeholder="SmileID">
    <input type="password" id="upw"
           autocomplete="off" placeholder="パスワード">
    <button type="button" id="login_btn" style="width:70px">
      Login
    </button>
  </div>
  
  <div id="toast_msg" class="hide"></div>

  <div id="user_info" style="width:800px; text-align:right; display:none;">
    <span style="font-size:small;"></span>
    <button type="button" id="logout_btn" style="width:70px">
      Logout
    </button>
  </div>

  <div id="audio_form" style="display:none;">
    <h2>step1 - スクリプト入力</h2>

    <div>
      最大 500文字。「、」「。」で80文字以内で区切ってください。
    </div>
    
    <textarea id="text" rows="12" cols="110"
          >音声合成ソフトの VOICEVOX に ブラウザ用画面を設け、複数名による同時利用を可能にしました。
※ VOICEVOX の規約に従い、作成した資料には「VOICEVOX:波音リツ」の記載をお願いします</textarea><br/>
      
    <button id="synthBtn">音声合成</button>
    <img id="loadingGif" src="loading.gif" style="display:none; margin-left:10px;"
     alt="読み込み中...">
    
    <h2>step2 - イントネーションPitch編集</h2>
    <div>
      音節(モーラ)のイントネーションPitchをで調整できます。
    </div>
    
    <table style="width:750px;">
      <thead>
        <tr>
          <th>スクリプト</th>
          <th style="width:100px">size</th>
          <th style="width:150px">time</th>
          <th style="width:60px">操作</th></tr>
      </thead>
      <tbody id="tbody"></tbody>
    </table>
    
    <div id="accentPanel"
     style="display:none; margin-top:10px; border:1px solid #ccc; padding:10px; background:#f9f9f9;">
      <div style="display:flex; align-items:center; gap:10px;">
        <audio id="accentAudio" controls preload="metadata" style="width:300px;"></audio>
        <button id="synthWithAccentBtn">この内容で合成</button>
        <button id="resetAccentBtn">初期化</button>
        <button id="closeAccentBtn">キャンセル</button>
      </div>
      <div id="org_text"></div>
      <div id="accentPhrases"></div>
      <div style="margin-top:10px; padding:5px; border-top:1px solid #ddd;">
        <label>この文の後の無音時間:
          <input type="number" id="pauseInput"
         min="0" max="5" step="0.5" value="0" style="width:60px;"></label>
      </div>
    </div>
    
    <h2>step3 - 音声ダウンロード</h2>
    <div id="mergedPanel" style="display:none; margin-top:10px;">
      <div style="margin-top:10px; width:300px;">
        <audio id="mergedAudio" controls preload="auto" style="width:100%;"></audio>
      </div>
    </div>
    
  </div><!--#audio_form-->

  <template id="rowTemplate">
    <tr>
      <td class="name"></td>
      <td style="text-align:center;" class="size"></td>
      <td style="text-align:center;" class="time"></td>
      <td style="text-align:center;">
        <button class="accentBtn">編集</button>
      </td>
    </tr>
  </template>
  <script src="voicevox.js"></script>
</body>
</html>

javascript

'use strict';
//const login_url = "../chk_user.html";
const login_url = "../chk_user.pl";
const allowed_depts = ["開発部","生産グループ"];
const voicevox_api  = "voicevox.pl";


class VoicevoxWavManager {
    constructor(tbodyId, textId, templateId) {
        this.tbody = document.getElementById(tbodyId);
        this.textElement = document.getElementById(textId);
        this.template = document.getElementById(templateId);
        this.audioQuery = null;
        this.queryText = null;
        this.sentenceQueries = []; // 分割した文字列とアクセント情報を保存
    }
    
    init_window() {
        this.add_event_listener();
    }

    add_event_listener=()=> {
        document.querySelector("#login_btn").addEventListener(
            'click',()=>{ this.login(); });
        document.querySelector("#logout_btn").addEventListener(
            'click',()=>{ this.logout(); });
        document.querySelector("#uid").addEventListener('keypress',(e)=>{
            if (e.keyCode == 13) this.login(); // ENTER
        });
        document.querySelector("#upw").addEventListener('keypress',(e)=>{
            if (e.keyCode == 13) this.login(); // ENTER
        });
        document.getElementById('synthBtn').addEventListener(
            'click',() => this.synth() );
        document.getElementById('synthWithAccentBtn').addEventListener(
            'click', () => { this.synthWithAccent(); });

        document.getElementById('resetAccentBtn').addEventListener(
            'click', () => { this.resetAccent(); });

        document.getElementById('closeAccentBtn').addEventListener(
            'click', () => {
                document.getElementById('accentPanel').style.display = 'none';
                this.currentEditingFile = null;
            });
    }
    
    toast_msg=(msg, msec)=>{
        if( msec==undefined ||  msec==null){
            msec = 5000;
        }
        let msg_container = document.querySelector("#toast_msg");
        msg_container.innerHTML = msg;
        
        msg_container.className = msg_container.className.replace("hide","");
        setTimeout(()=>{ msg_container.className="hide";},msec);
    }
    
    login =async()=> {
        let form_data = new URLSearchParams();
        form_data.append('id', document.querySelector("#uid").value);
        form_data.append('pw', document.querySelector("#upw").value);

        let response;
        try {
            if( login_url.match(/.*\.pl$/) ){
                response = await fetch(login_url,
                                       {method:"POST",
                                        body:form_data.toString() });
            } else {
                response = await fetch(login_url);
            }
        } catch (error) {
            this.toast_msg("認証システムが応答せず、ログイン処理に失敗しました");
            return undefined;
        }
        if (! response.ok ){
            this.toast_msg("ユーザIDまたは、パスワードが誤っているようです");
            return undefined;
        }

        let res_txt =
            new TextDecoder('utf-8').decode( await response.arrayBuffer()) ||"";
        let user_info = res_txt.split("\t");
        if(user_info.length < 2){
            this.toast_msg("ユーザの所属部署を確認できない為、ご利用頂けません");
            return undefined;
        }

        let use_ok = false;
        for(let allowed_dept of allowed_depts ){
            if( user_info[1].indexOf( allowed_dept ) >= 0){
                use_ok = true;
                break;
            }
        }
        
        if(! use_ok){
            this.toast_msg(
                "このシステムは住宅カンパニー開発統括部の方のみ、利用可能です");
            return undefined;
        }

        let user_info_str = user_info.join("@");
        document.querySelector("#user_info span").textContent = user_info_str;

        document.querySelector("#user_info").style.display = "block";
        document.querySelector("#login_form").style.display= "none";

        document.querySelector("#audio_form").style.display = "block";
    }
    
    logout() {
        document.querySelector("#user_info span").textContent = "";
        document.querySelector("#uid").value = "";
        document.querySelector("#upw").value = "";
        
        document.querySelector("#audio_form").style.display = "none";
        
        document.querySelector("#user_info").style.display = "none";
        document.querySelector("#login_form").style.display= "block";
        document.querySelector("#uid").focus();
    }
    

    getLoginUser(){
        const user_info =
              document.querySelector("#user_info span").textContent.split("@");
        return user_info[0].trim();
    }
    
    synth=async()=>{
        const uid = this.getLoginUser();

        const text = this.textElement.value;
        if (text.length == 0) {
            alert('音声合成するスクリプトを入力してください');
            return;
        }

        // 音声合成済みfileを一旦、全削除
        await this.delUsersList();

        // ファイルがない場合もtableを空にしてから音声合成を開始
        this.tbody.innerHTML = '';

        try {
            this.startSynthesis(uid, text);
        } catch (error) {
            alert(error.message);
        }
    }
    
    startSynthesis=async(uid, text)=>{
        // ローディングGIFを表示
        const loadingGif = document.getElementById('loadingGif');
        if (loadingGif) loadingGif.style.display = 'inline';

        try {
            // アクセント編集情報をリセット
            this.audioQuery = null;
            this.queryText = null;
            this.sentenceQueries = []; // 保存されたアクセント情報をクリア
            this.mergedFile = null;
            document.getElementById('accentPanel').style.display = 'none';
            document.getElementById('mergedPanel').style.display = 'none';

            // 2. 「、」「。」で最大80文字以内に分割
            const sentences = this.splitText(text, 80);

            let i = 0;
            for( const sentence of sentences ){

                const accent = await this.calcAccent(uid,sentence);

                const wav_json =
                      await this.accent_to_wav( uid, accent, "", sentence );
                
                this.sentenceQueries.push({index  : i,
                                           text   : sentence,
                                           query  : accent,
                                           wavFile: wav_json.file,
                                           size   : wav_json.size,
                                           time   : wav_json.time,
                                           pause  : 0});

                const tr = this.addTblTr( {index  : i,
                                           text   : sentence,
                                           query  : accent,
                                           wavFile: wav_json.file,
                                           size   : wav_json.size,
                                           time   : wav_json.time});
                this.tbody.appendChild(tr);
                i++;
            }

            // 全ての音声合成が完了したら、WAVファイルを結合
            await this.mergeAllWavFiles(uid);
        } finally {
            // ローディングGIFを非表示
            if (loadingGif) loadingGif.style.display = 'none';
        }
    }

    mergeAllWavFiles=async(uid)=>{
        // sentenceQueriesからファイル名とpause情報を作成
        // pause値は既にsentenceQueriesに保存されている
        const filesWithPause = this.sentenceQueries.map(sq => ({
            file: sq.wavFile,
            pause: sq.pause
        })).filter(item => item.file);

        if (filesWithPause.length === 0) {
            console.log('結合するファイルがありません');
            return;
        }

        console.log('結合するファイル:', filesWithPause);

        const req_params = new URLSearchParams({
            action: 'merge',
            uid: uid,
            json: JSON.stringify({ files: filesWithPause })
        });
    const req_url = voicevox_api +"?action=merge";
        try {
            const response = await fetch(req_url, {
                method: 'POST',
                body: req_params
            });
            const result = await response.json();

            if (result.status === 'ok' && result.file) {
                console.log('結合完了:', result.file);

                // 結合済みパネルを表示
                const mergedPanel = document.getElementById('mergedPanel');
                const mergedAudio = document.getElementById('mergedAudio');

                // キャッシュを無効化して読み込み
                mergedAudio.src = 'wav/' + result.file + '?' + Date.now();
                mergedAudio.load();

                mergedPanel.style.display = 'block';

                // 結合ファイル名を保存
                this.mergedFile = result.file;
            }
        } catch (error) {
            console.error('結合エラー:', error);
        }
    }

    addTblTr( trInfo ){
        const clone = this.template.content.cloneNode(true);
        const tr = clone.querySelector('tr');

        const nameCell = tr.querySelector('.name');
        nameCell.textContent = trInfo.text;

        const sizeInKB = (parseInt(trInfo.size) / 1024).toFixed(1);
        tr.querySelector('.size').textContent = sizeInKB + ' KB';

        tr.querySelector('.time').textContent = trInfo.time;

        const accentBtn = tr.querySelector('.accentBtn');
        accentBtn.addEventListener('click', () => {
            this.editAccentForFile( trInfo );
        });
        return tr;
    }

    cleanText(text) {
        // 改行と全角・半角スペースを除去
        return text.replace(/[\r\n\s ]+/g, '');
    }

    splitText(text, maxLength) {
        // 1. 改行と空白を除去
        const cleanedText = this.cleanText(text);

        if (cleanedText.length > 500) {
            throw new Error('テキストが長すぎます');
        }

        // 3. 「。」で分割
        const sentences =
              cleanedText.split('。').filter(s => s.length>0).map(s=> s +'。');

        // 最後の要素が「。」で終わらない場合の処理
        if (sentences.length > 0 && !cleanedText.endsWith('。')) {
            sentences[sentences.length - 1] =
                sentences[sentences.length - 1].slice(0, -1);
        }

        const result = [];

        // 4. 80文字を超える文を「、」で分割
        for (const sentence of sentences) {
            if (sentence.length <= maxLength) {
                result.push(sentence);
            } else {
                // 「、」で分割
                const parts = sentence.split('、').filter(p => p.length > 0);
                let combined = '';

                for (let i = 0; i < parts.length; i++) {
                    const part = parts[i] + (i < parts.length - 1 ? '、' : '');

                    // 5. 「、」で分割後も80文字を超える場合はエラー
                    if (part.length > maxLength) {
                        throw new Error(
                            `分割後も80文字超です:${part.substring(0, 20)}..`);
                    }

                    if ((combined + part).length <= maxLength) {
                        combined += part;
                    } else {
                        if (combined.length > 0) {
                            result.push(combined);
                        }
                        combined = part;
                    }
                }

                if (combined.length > 0) {
                    result.push(combined);
                }
            }
        }

        return result;
    }
    
    calcAccent=async(uid,sentence)=>{
        const req_params = new URLSearchParams({action: 'audio_query',
                                                uid :  uid,
                                                text: sentence });
    const req_url = voicevox_api +"?action=audio_query";
        const response = await fetch(req_url,
                     {method: 'POST',
                                      body: req_params});
        const res_json = await response.json();
        return res_json.query;
    }
    
    synthWithAccent=async()=>{
        if (!this.audioQuery || !this.currentEditingFile) {
            alert('アクセント情報が見つかりません');
            return;
        }

        // ローディングGIFを表示
        const loadingGif = document.getElementById('loadingGif');
        if (loadingGif) loadingGif.style.display = 'inline';

        try {
            const uid = this.getLoginUser();
            const accent = this.audioQuery;
            const originalFilename = this.currentEditingFile;
            const originalText = this.queryText;

            // デバッグ: POSTする前のデータを確認
            console.log('synthWithAccent - POSTアクセント:',
                        JSON.stringify(accent));
            console.log('synthWithAccent - 対象ファイル:', originalFilename);
            console.log('synthWithAccent - 元のテキスト:', originalText);

            // 元のファイル名と元のテキストを指定して上書き
            const wav_json = await this.accent_to_wav(uid,
                                                      accent,
                                                      originalFilename,
                                                      originalText);

            if (wav_json.status === 'ok' && wav_json.file) {
                console.log('アクセント編集後の再合成完了:', wav_json);

                // sentenceQueriesの該当データを更新
                const queryData = this.sentenceQueries.find(
                    sq => sq.wavFile === originalFilename);
                if (queryData) {
                    queryData.query = accent;
                    queryData.size = wav_json.size;
                    queryData.time = wav_json.time;

                    // 無音時間の値を保存
                    const pauseInput = document.getElementById('pauseInput');
                    if (pauseInput) {
                        queryData.pause = parseFloat(pauseInput.value) || 0;
                    }
                }

                // tableの該当行を更新
                const rows = this.tbody.querySelectorAll('tr');
                rows.forEach((row, index) => {
                    const queryItem = this.sentenceQueries[index];
                    if (queryItem && queryItem.wavFile === originalFilename) {
                        row.querySelector('.size').textContent =
                            (parseInt(wav_json.size) / 1024).toFixed(1) + ' KB';
                        row.querySelector('.time').textContent = wav_json.time;
                    }
                });

                // アクセント編集パネルのaudio要素を更新
                const audio = document.getElementById('accentAudio');
                if (audio) {
                    // キャッシュを無効化し再読み込み
                    audio.src = 'wav/' + originalFilename + '?' + Date.now();
                    audio.load();
                }

                // マージファイルを再生成(無音時間を反映)
                await this.mergeAllWavFiles(uid);
            }

        } finally {
            // ローディングGIFを非表示
            if (loadingGif) loadingGif.style.display = 'none';
        }
    }

    resetAccent=async()=>{
        if (!this.queryText || !this.currentEditingFile) {
            alert('初期化する対象が見つかりません');
            return;
        }

        // ローディングGIFを表示
        const loadingGif = document.getElementById('loadingGif');
        if (loadingGif) loadingGif.style.display = 'inline';

        try {
            const uid = this.getLoginUser();
            const originalFilename = this.currentEditingFile;
            const originalText = this.queryText;

            console.log('=== アクセントを初期化(VOICEVOXデフォルト) ===');
            console.log('対象ファイル:', originalFilename);
            console.log('テキスト:', originalText);

            // VOICEVOXのデフォルトでアクセント計算
            const newAccent = await this.calcAccent(uid, originalText);

            // 新しいアクセント情報で音声合成
            const wav_json = await this.accent_to_wav(uid,
                                                      newAccent,
                                                      originalFilename,
                                                      originalText);

            if (wav_json.status === 'ok' && wav_json.file) {
                console.log('初期化完了:', wav_json);

                // sentenceQueriesの該当データを更新
                const queryData = this.sentenceQueries.find(
                    sq => sq.wavFile === originalFilename);
                if (queryData) {
                    queryData.query = newAccent;
                    queryData.size = wav_json.size;
                    queryData.time = wav_json.time;
                }

                // audioQueryを更新
                this.audioQuery = newAccent;

                // tableの該当行を更新
                const rows = this.tbody.querySelectorAll('tr');
                rows.forEach((row, index) => {
                    const queryItem = this.sentenceQueries[index];
                    if (queryItem && queryItem.wavFile === originalFilename) {
                        row.querySelector('.size').textContent =
                            (parseInt(wav_json.size) / 1024).toFixed(1) + ' KB';
                        row.querySelector('.time').textContent = wav_json.time;
                    }
                });

                // アクセント編集パネルを再表示(新しいアクセント情報で)
                this.showAccentEditorForFile();

                // マージファイルを再生成
                console.log('初期化後、マージファイルを再生成します');
                await this.mergeAllWavFiles(uid);
            }
        } finally {
            // ローディングGIFを非表示
            if (loadingGif) loadingGif.style.display = 'none';
        }
    }


    accent_to_wav=async(uid,accent,filename,text='')=>{
        const queryString = JSON.stringify(accent);

        const req_params = new URLSearchParams({
            action: 'synth_with_query',
            uid :  uid,
            query: queryString,
            filename: filename,
            text: text
        });

    const req_url = voicevox_api +"?action=synth_with_query";
        const response = await fetch(req_url,{method: 'POST',
                                              body: req_params});
        const res_json = await response.json();
        console.log('accent_to_wav - サーバーレスポンス:', res_json);
        return res_json;
    }
    
    delUsersList=async(uid)=> {
        let req_params = new URLSearchParams({action: "delete_all",
                                              uid   : this.getLoginUser()});
        let req_url = voicevox_api +"?"+ req_params;
        let response = await fetch(req_url, {method:"GET"});
        return response.json();
    }

    

    editAccentForFile(queryData) {
        // ファイルに紐付いたアクセント情報を編集
        this.audioQuery = queryData.query;
        this.queryText = queryData.text;
        this.currentEditingFile = queryData.wavFile;
        
        this.showAccentEditorForFile();
    }

    showAccentEditorForFile() {
        const panel = document.getElementById('accentPanel');
        const container = document.getElementById('accentPhrases');
        container.innerHTML = '';

        // audio要素を設定(キャッシュ無効化)
        const audio = document.getElementById('accentAudio');
        audio.src = 'wav/' + this.currentEditingFile + '?' + Date.now();
        audio.load();

        // 元のテキストを表示
        document.getElementById('org_text').textContent = this.queryText;

        // 無音時間の入力フィールドに現在の値を設定
        const pauseInput = document.getElementById('pauseInput');
        const queryData = this.sentenceQueries.find(
            sq => sq.wavFile === this.currentEditingFile);
        if (pauseInput && queryData) {
            pauseInput.value = queryData.pause || 0;
        }

        this.audioQuery.accent_phrases.forEach((phrase, phraseIdx) => {
            const phraseDiv = document.createElement('div');
            phraseDiv.className = 'phrase';
            phraseDiv.style.marginBottom = '2px';

            phrase.moras.forEach((mora, moraIdx) => {
                // モーラ表示用のコンテナ
                const moraContainer = document.createElement('div');
                moraContainer.style.display = 'inline-block';
                moraContainer.style.marginRight = '2px';
                moraContainer.style.textAlign = 'center';

                // モーラのテキスト
                const moraSpan = document.createElement('div');
                moraSpan.className = 'mora';
                moraSpan.textContent = mora.text;
                moraSpan.style.fontSize = '16px';
                moraSpan.style.fontWeight = 'bold';
                moraSpan.style.marginBottom = '2px';

                // pitch入力フィールド
                const pitchInput = document.createElement('input');
                pitchInput.type = 'number';
                pitchInput.min = '0';
                pitchInput.max = '10';
                pitchInput.step = '0.05';
                pitchInput.value = mora.pitch.toFixed(2);
                pitchInput.style.width = '40px';
                pitchInput.style.fontSize = '12px';
                pitchInput.dataset.phraseIdx = phraseIdx;
                pitchInput.dataset.moraIdx = moraIdx;

                // pitch値が変更されたときの処理
                pitchInput.addEventListener('input', (e) => {
                    const newPitch = parseFloat(e.target.value);
                    if (!isNaN(newPitch)) {
                        this.audioQuery.accent_phrases[phraseIdx].moras[moraIdx].pitch = newPitch;
                        // sentenceQueriesも更新
                        const queryData = this.sentenceQueries.find(
                            sq => sq.wavFile === this.currentEditingFile);
                        if (queryData) {
                            queryData.query = this.audioQuery;
                        }
                    }
                });

                moraContainer.appendChild(moraSpan);
                moraContainer.appendChild(pitchInput);
                phraseDiv.appendChild(moraContainer);
            });

            container.appendChild(phraseDiv);
        });
        panel.style.display = 'block';
    }
}

// インスタンス作成とイベント設定
const manager = new VoicevoxWavManager('tbody', 'text', 'rowTemplate');
manager.init_window();

perl cgi

#!C:/Strawberry/perl/bin/perl
use strict;
use warnings;
use utf8;
use CGI;
use JSON qw(encode_json decode_json);
use LWP::UserAgent;
use URI::Escape qw(uri_escape_utf8);
use File::Spec;
use POSIX qw(strftime);
use Encode qw(decode);

binmode STDOUT, ':raw';

my $VOICEVOX = 'http://127.0.0.1:50021';
my $BASE_DIR = 'C:/inetpub/wwwroot/tts';
my $WAV_DIR  = "$BASE_DIR/wav";
my $SPEAKER  = 9; # 波音リツ

main();

sub main {
    mkdir $WAV_DIR unless -d $WAV_DIR;

    my $q = CGI->new;
    $q->charset('UTF-8');
    my $ua = LWP::UserAgent->new(timeout => 30);
    
    my $action = $q->param('action') || '';
    my $uid = $q->param('uid') || 'unknown_user';
    
    # step 0 全削除
    if ($action eq 'delete_all') {
        return delete_all($q, $uid);
    }
    # step 1 元textから 音声合成用のクエリ作成 ( アクセント取得 )
    if ($action eq 'audio_query') {
        return audio_query($q,$ua);
    }
    # step 2 クエリ付き合成
    if ($action eq 'synth_with_query') {
        return synth_with_query($q,$ua,$uid);
    }
    # step 3 そのユーザの複数のwavを結合
    if ($action eq 'merge') {
        return merge($q, $ua,$uid);
    }
    return json_out($q,{status=>'error',msg=>'unknown action'});
}

# 元textから 音声合成用クエリ作成 ( アクセント取得 )
sub audio_query {
    my ($q, $ua) = @_;

    my $text = decode('UTF-8', $q->param('text'));
    my $uid  = $q->param('uid') || 'unknown_user';

    my $enc = uri_escape_utf8($text);
    # refer to https://voicevox.github.io/voicevox_engine/api/
    my $url = "$VOICEVOX/audio_query?text=$enc&speaker=$SPEAKER";

    my $rq = $ua->post($url);
    error_out("audio_query failed") unless $rq->is_success;

    my $qjson = decode_json($rq->decoded_content);
    json_out($q,{status=>'ok',query=>$qjson,debug_text=>$text});
}

# 音声合成用クエリから音声合成 ( wav作成 )
sub synth_with_query {
    my ($q, $ua, $uid) = @_;

    my $query_str = $q->param('query') // '';
    return error_out('query empty') if ($query_str eq '');
    
    my $rs = $ua->post("$VOICEVOX/synthesis?speaker=$SPEAKER",
                       Content_Type => 'application/json',
                       Content => $query_str );
    return error_out("synthesis failed") unless $rs->is_success;

    my $filename = $q->param('filename') // '';
    # filenameパラメータがなければ連番で新規生成
    if ($filename eq '' or not $filename =~ /\.wav$/o ) {
        my $max_num = 0;
        opendir my $dh, $WAV_DIR or error_out("open dir fail");
        for my $f (readdir $dh) {
            if ( $f =~ /^${uid}_part(\d+)\.wav$/ ) {
                my $num = int($1);
                $max_num = $num if $num > $max_num;
            }
        }
        closedir $dh;
        $filename = sprintf("%s_part%02d.wav", $uid, $max_num + 1);
    }
    my $path = File::Spec->catfile($WAV_DIR,$filename);

    # wav fileとして保存
    open my $fh,'>:raw',$path or error_out("cannot write wav");
    print $fh $rs->content;
    close $fh;

    my @stat = stat($path);

    json_out($q,
             {status=>'ok',
              file=>$filename,
              size=>$stat[7],
              time=>strftime("%y-%m-%d %H:%M:%S",localtime($stat[9]))});
}

sub merge {
    my ($q, $ua, $uid) = @_;

    my $data = decode_json($q->param('json')||'{}');
    my @files=@{$data->{files}||[]};
    return error_out("no files") unless @files;

    my $out="${uid}_merge.wav";
    my $op=File::Spec->catfile($WAV_DIR,$out);

    my $header = '';
    my $total_data = '';
    my $sample_rate = 24000;  # VOICEVOX default
    my $channels = 1;         # モノラル
    my $bits_per_sample = 16; # 16bit

    for my $item (@files) {
        # 新形式: {file: "xxx.wav", pause: 1.5} または 旧形式: "xxx.wav"
        my $f = ref($item) eq 'HASH' ? $item->{file} : $item;
        my $pause_sec = ref($item) eq 'HASH' ? ($item->{pause} || 0) : 0;

        my $path = File::Spec->catfile($WAV_DIR,$f);
        open my $I,'<:raw',$path or next;

        my $h;
        read($I, $h, 44);

        # 最初のファイルのヘッダーを保存してサンプルレートを取得
        if ($header eq '') {
            $header = $h;
            # サンプルレート取得(byte 24-27)
            $sample_rate = unpack('V', substr($h, 24, 4));
            # チャンネル数取得(byte 22-23)
            $channels = unpack('v', substr($h, 22, 2));
            # ビット深度取得(byte 34-35)
            $bits_per_sample = unpack('v', substr($h, 34, 2));
        }

        # データ部分を読み込んで結合
        my $buf;
        while(read($I, $buf, 8192)) {
            $total_data .= $buf;
        }
        close $I;

        # pause_secが指定されている場合、無音データを追加
        if ($pause_sec > 0) {
            my $bytes_per_sample = $bits_per_sample / 8;
            my $silence_samples = int($sample_rate * $pause_sec);
            my $silence_bytes = $silence_samples * $channels * $bytes_per_sample;

            # 16bit符号付き整数の無音(値0)
            $total_data .= pack('v*', (0) x ($silence_samples * $channels));
        }
    }

    # ヘッダーのサイズ情報を更新
    my $data_size = length($total_data);
    substr($header, 4, 4) = pack('V', $data_size + 36);  # ファイルサイズ - 8
    substr($header, 40, 4) = pack('V', $data_size);      # データサイズ

    open my $O,'>:raw',$op or error_out("merge create fail");
    print $O $header;
    print $O $total_data;
    close $O;

    json_out($q,{status=>'ok',file=>$out});
}

sub delete_all {
    my ($q, $uid) = @_;

    opendir my $dh,$WAV_DIR or error_out("open dir fail");
    my $cnt=0;
    for my $f (readdir $dh) {
        next unless $f =~ /$uid\_.*\.wav$/;
        
        my $p = File::Spec->catfile($WAV_DIR,$f);
        if (-f $p) { unlink $p; $cnt++; }
    }
    closedir $dh;
    json_out($q,{status=>'ok',deleted=>$cnt});
}

sub json_out {
    my ($q,$d) = @_;
    print $q->header(-type=>'application/json',-charset=>'utf-8');
    print encode_json($d);
    exit;
}

sub error_out { json_out({status=>'error',msg=>shift}); }

u

http request from python to qwen3-vl via ollama

text requestの練習

import requests

API_SERVER_URL = "http://localhost:11434/api/chat"

def main():
    headers = {"Content-Type": "application/json"}
    json = {
        "model": "qwen3-vl:2b",
        "stream": False,
        "messages": [
            {"role": "user", "content": "日本の首都はどこ?"}
        ],
        "options": {
            "hide_thinking": True
        }
    }

    response = requests.post(API_SERVER_URL, headers=headers, json=json, timeout=60)
    response.raise_for_status()

    #print(response.text)
    data = response.json()
    print( data["message"]["content"] )
    

if __name__ == '__main__':
    main()

image requestの練習

import requests
import base64

API_SERVER_URL = "http://localhost:11434/api/chat"

def main():
    # 説明させたい画像ファイル
    image_path = "house_floor_plan_1f.png"

    with open(image_path, "rb") as f:
        image_base64 = base64.b64encode(f.read()).decode("utf-8")

    payload = {
        "model": "qwen3-vl:2b",
        "stream": False,
        "messages": [
            {"role": "user",
             "content": "この画像の内容を日本語で説明してください",
             "images": [image_base64]
             }
        ],
        "options": { "hide_thinking": True}
    }

    response = requests.post(API_SERVER_URL, json=payload)
    response.raise_for_status()

    result = response.json()
    print(result["message"]["content"])
    

if __name__ == '__main__':
    main()

autocadの autolispマクロの練習 - その3

Help Help

AutoCADの PUBLISHコマンドで、複数のdwgを連続してpdf化する為、 予めdir内にあるdwgファイル群の印刷スタイルを白黒 & A3横化

; SETPLOTCFG command
; Set plot configuration for DWG files
; - Printer: AutoCAD PDF (General Documentation).pc3
; - Style table: monochrome.ctb
(defun c:setplotcfg ( / dwgdir files f fullpath )
  (princ "\nSTART SETPLOTCFG()")
  (vl-load-com)

  (setq dwgdir "c:/Users/end0t/dev/AUTOLISP/DWG")

  (setq files (vl-directory-files dwgdir "*.dwg" 1))
  (foreach f files
    (setq fullpath (strcat dwgdir "/" f))
    (set-plot-config fullpath)
  )

  (princ "\nDONE SETPLOTCFG()")
  (princ)
)

; Open DWG and set plot config for non-Model layouts
(defun set-plot-config ( dwgpath / acad docs doc layouts lay lay-name dwgname changed )
  (vl-load-com)

  (setq acad (vlax-get-acad-object))
  (setq docs (vla-get-Documents acad))

  (setq doc (vla-open docs dwgpath))
  (setq dwgname (vla-get-Name doc))
  (setq changed nil)

  (princ (strcat "\n\nDWG: " dwgname))

  (setq layouts (vla-get-Layouts doc))

  (vlax-for lay layouts
    (setq lay-name (vla-get-Name lay))

    (if (/= lay-name "Model")
      (progn
        (princ (strcat "\n  Layout: " lay-name))

        ; Printer/Plotter
        (vla-put-ConfigName lay "AutoCAD PDF (General Documentation).pc3")
        (princ "\n    ConfigName: AutoCAD PDF (General Documentation).pc3")

        ; Paper size: ISO full bleed A3 (420 x 297 mm)
        (vla-put-CanonicalMediaName lay "ISO_full_bleed_A3_(420.00_x_297.00_MM)")
        (princ "\n    PaperSize: ISO full bleed A3 (420 x 297 MM)")

        ; Plot area: Extents (1=Extents)
        (vla-put-PlotType lay 1)
        (princ "\n    PlotType: Extents")

        ; Scale 1:1
        (vla-put-UseStandardScale lay :vlax-true)
        (vla-put-StandardScale lay 16)
        (princ "\n    Scale: 1:1")

        ; Center plot
        (vla-put-CenterPlot lay :vlax-true)
        (princ "\n    CenterPlot: TRUE")

        ; Plot rotation: Landscape (0=0, 1=90, 2=180, 3=270)
        (vla-put-PlotRotation lay 0)
        (princ "\n    PlotRotation: Landscape")

        ; Style table
        (vla-put-StyleSheet lay "monochrome.ctb")
        (princ "\n    StyleSheet: monochrome.ctb")

        ; Use plot styles
        (vla-put-PlotWithPlotStyles lay :vlax-true)
        (princ "\n    PlotWithPlotStyles: TRUE")

        ; Plot lineweights
        (vla-put-PlotWithLineweights lay :vlax-true)
        (princ "\n    PlotWithLineweights: TRUE")

        ; Plot paper space last
        (vla-put-PlotViewportBorders lay :vlax-false)
        (princ "\n    PlotViewportBorders: FALSE")

        (setq changed T)
      )
    )
  )

  (if changed
    (progn
      (vla-save doc)
      (princ "\n  -> Saved.")
    )
  )

  (vla-close doc :vlax-false)
)

autocadの autolispマクロの練習 - その2

Help Help

modelレイヤにある直径18の円を17へ変更。

更に付随する寸法線や、円の直情にある断面を表す破線も移動

(defun c:chgcircle ( / dwgdir files f fullpath )
  (princ "\nSTART CHGCIRCLE()")
  ; ActiveX のサポートに関連がある拡張 AutoLISP 関数を読込み
  (vl-load-com)

  (setq dwgdir "c:/Users/end0t/dev/AUTOLISP/DWG2")

  (setq files (vl-directory-files dwgdir "*.dwg" 1))
  (foreach f files
    (setq fullpath (strcat dwgdir "/" f))
    (change-circle-diameter fullpath)
  )

  (princ "\nDONE CHGCIRCLE()")
  (princ)
)

; modelレイヤにある直径18の円を17へ変更.
; 付随する寸法線も 17へ変更
; 円周上部のDASHED垂直線も中心方向へ0.5移動
(defun change-circle-diameter
       ( dwgpath
         / acad docs doc layouts lay blk obj dwgname
           diameter changed-count circle-centers )

  (vl-load-com)
  ; AutoCAD objectと dwg一覧 objectの取得
  (setq acad (vlax-get-acad-object))
  (setq docs (vla-get-Documents acad))

  (setq doc (vla-open docs dwgpath))
  (setq dwgname (vla-get-Name doc))
  (setq changed-count 0)
  (setq circle-centers nil)

  (princ (strcat "\n\nDWG: " dwgname))

  (setq layouts (vla-get-Layouts doc))

  (vlax-for lay layouts
    (if (= (vla-get-Name lay) "Model")
      (progn
        (princ (strcat "\n Layout: " (vla-get-Name lay)))
        (setq blk (vla-get-Block lay))

        (if blk
          (progn
            ; 直径18の円を探して変更し、中心座標を記録
            (vlax-for obj blk
              (if (= (vla-get-ObjectName obj) "AcDbCircle")
                (progn
                  (setq diameter (* 2.0 (vla-get-Radius obj)))
                  (if (< (abs (- diameter 18.0)) 0.001)
                    (progn
                      ; 中心座標を記録し、sizeを17へ変更
                      (setq circle-centers
                        (cons (vlax-get obj 'Center) circle-centers))
                      (vla-put-Radius obj 8.5)
                      (vla-Update obj)
                      (princ "\n    -> Changed to diameter=17")
                      (setq changed-count (1+ changed-count))
                    )
                  )
                )
              )
            )

            ; 円周上部のDASHED垂直線を移動
            (if circle-centers
              (move-dashed-lines-toward-center blk circle-centers 9.0 0.5)
            )
          )
        )
      )
    )
  )

  (princ (strcat "\n  Changed " (itoa changed-count) " circle(s)"))
  (if (> changed-count 0)
    (progn
      (update-dimension-text blk 18.0 "17")

      (vla-save doc)
      (princ "\n  Saved.")
    )
  )

  (vla-close doc :vlax-false)
)

; 円周上部のDASHED垂直線を中心方向へ移動
; blk: ブロック
; circle-centers: 円の中心座標リスト
; radius: 元の半径(円周上のY座標判定用)
; move-dist: 移動距離
(defun move-dashed-lines-toward-center
       ( blk circle-centers radius move-dist
         / obj center cx cy start-pt end-pt sx sy ex ey
           linetype-name is-vertical is-on-circle-top new-x )

  (vlax-for obj blk
    (if (= (vla-get-ObjectName obj) "AcDbLine")
      (progn
        ; 線種名を取得(DASHEDかどうか)
        (setq linetype-name (strcase (vla-get-Linetype obj)))

        (if (wcmatch linetype-name "*DASHED*")
          (progn
            (setq start-pt (vlax-get obj 'StartPoint))
            (setq end-pt (vlax-get obj 'EndPoint))
            (setq sx (car start-pt))
            (setq sy (cadr start-pt))
            (setq ex (car end-pt))
            (setq ey (cadr end-pt))

            ; 垂直線かどうか判定(X座標がほぼ同じ)
            (setq is-vertical (< (abs (- sx ex)) 0.001))

            (if is-vertical
              (progn
                ; 各円の中心に対してチェック
                (foreach center circle-centers
                  (setq cx (car center))
                  (setq cy (cadr center))

                  ; 円周上部にあるかどうか(Y座標が中心+半径付近)
                  (setq is-on-circle-top
                    (and
                      ; 線のX座標が円周上(中心からradius離れている)
                      (< (abs (- (abs (- sx cx)) radius)) 0.5)
                      ; 線のY座標が円の上部(中心より上)
                      (> sy cy)
                    )
                  )

                  (if is-on-circle-top
                    (progn
                      ; 中心方向へ移動(X座標を中心に近づける)
                      (if (> sx cx)
                        ; 線が中心より右にある場合、左へ移動
                        (setq new-x (- sx move-dist))
                        ; 線が中心より左にある場合、右へ移動
                        (setq new-x (+ sx move-dist))
                      )
                      (vla-put-StartPoint obj
                        (vlax-3d-point new-x sy (caddr start-pt)))
                      (vla-put-EndPoint obj
                        (vlax-3d-point new-x ey (caddr end-pt)))
                      (vla-Update obj)
                      (princ (strcat "\n    -> Moved DASHED line toward center by " (rtos move-dist 2 1)))
                    )
                  )
                )
              )
            )
          )
        )
      )
    )
  )
)

; 直径を変更しても、付随する寸法値が、なぜか変更されない為、強制的に置換
(defun update-dimension-text
       ( blk old-meas new-val-str
         / obj meas override new-override )

  (vlax-for obj blk
    (if (wcmatch (vla-get-ObjectName obj) "AcDb*Dimension*")
        (progn
        (setq meas (vla-get-Measurement obj))
        (if (< (abs (- meas old-meas)) 0.001)
          (progn
            (setq override (vla-get-TextOverride obj))
            (princ (strcat "\n  DIM: Measurement=" (rtos meas 2 3)
                           " TextOverride=" override))
            (if (/= override "")
              (progn
                (setq new-override
                  (vl-string-subst
                    (strcat "%%c" new-val-str)
                    "<>"
                    override))
                (vla-put-TextOverride obj new-override)
                (princ (strcat "\n    -> TextOverride=" new-override))
              )
              (progn
                (vla-put-TextOverride obj (strcat "%%c" new-val-str))
                (princ (strcat "\n    -> TextOverride=%%c" new-val-str))
              )
            )
          )
        )
      )
    )
  )
)

windows bat の練習

UAC 無効化の練習

rem @echo off は、コマンド自体を画面に表示しません
@echo off

echo 処理を開始します
rem echo; は、空白行の表示
echo;

rem openfiles は 管理者権限が必要な為、これを利用し、権限確認
openfiles > NUL 2>&1 
if NOT %ERRORLEVEL% EQU 0 (
  echo 管理者権限で実行してください
  pause
  goto EXIT
)

rem EnableLUAは、UAC の設定が入っているレジストリキー
set REGKEY=HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System
set REGNAME=EnableLUA

rem 現在のレジストリ値の参照
FOR /F "TOKENS=1,2,*" %%I IN (
     'REG QUERY "%REGKEY%" /v "%REGNAME%"'
  ) DO IF "%%I"=="%REGNAME%" SET VAL1=%%K
  
if "%VAL1%" EQU "0x0" (
  echo 既にUACは無効の為、処理不要です
  echo;
  pause
  goto EXIT
)

pause
echo UACを無効化します。
reg add HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System /v EnableLUA /t REG_DWORD /d 0 /f
echo UACを無効化しました。
echo;

echo PCを再起動してください
pause

:EXIT

各種インストーラ実行の練習

@echo off
rem 「echo.」は空白行の表示
echo.
echo 標準ソフトのバージョンアップを開始します。
echo.

rem AdobeReaderインストール
rem  このbatがあるdir(%~dp0)を再帰探索(/r)
rem 「START /WAIT」で、インストール完了まで待つ

for /r %~dp0 %%A in (AcroRdrDC*.exe) do (
START /WAIT %%A  /sAll /rs /norestart EULA_ACCEPT=YES
)

rem CubeICE
for /r %~dp0 %%A in (cubeice-*.exe) do (
START /WAIT %%A  /s /lang=japanese /verysilent /sp- /nocancel /norestart /suppressmsgboxes /nolaunch
)

rem CubePDF
:for /r %~dp0 %%A in (cubepdf-*.exe) do (
:START /WAIT %%A  /s /lang=japanese /verysilent /sp- /nocancel /norestart /suppressmsgboxes /nolaunch
:)

rem CubePDF Utiluity
for /r %~dp0 %%A in (cubepdf-utility*.exe) do (
START /WAIT %%A  /s /lang=japanese /verysilent /sp- /nocancel /norestart /suppressmsgboxes /nolaunch
)

rem java
for /r %~dp0 %%A in (jre-*-i586.exe) do (
START /WAIT %%A /s SPONSORS=0 REMOVEOUTOFDATEJRES=1
)

rem firefox
for /r %~dp0 %%A in (Firefox_Setup*.exe) do (
START /WAIT %%A /silent /install
)

rem ThunderBird
for /r %~dp0 %%A in (Thunderbird_Setup*.exe) do (
START /WAIT %%A /silent /install
)

echo 全てのインストールが完了しました
echo 何かキーを押して、画面を閉じてください
pause
exit

autocadの autolispマクロの練習

modelレイヤ以外にあるブロック図の表の属性を空欄化。 属性を用いず、TEXTやMTEXTは、座標で検索し、削除

; AutoCADから CONVDWGS というコマンドで実行
(defun c:convdwgs ( / dwgdir files f fullpath logfh logfile )
  (princ "START CONVDWGS()\n")
  (vl-load-com)

  (setq dwgdir "c:/Users/end0t/dev/AUTOLISP/DWG")
  (setq logfile (strcat dwgdir "/convdwg.log"))
  (setq logfh (open logfile "a"))

  ; dwgdir にある 各 *.dwgファイルを処理
  (setq files (vl-directory-files dwgdir "*.dwg" 1))
  (foreach f files
    (setq fullpath (strcat dwgdir "/" f))
    (conv-dwg fullpath logfh)
  )
  (close logfh)

  (princ "DONE CONVDWG()\n")
  (princ)
)

(defun sleep-ms (ms / t0)
  (setq t0 (getvar "DATE"))
  (while (< (* 86400000.0 (- (getvar "DATE") t0)) ms))
)

(defun log-write (fh msg)
  (write-line msg fh)
)

(defun point-in-range (pt min-x min-y max-x max-y / x y)
  (setq x (car pt))
  (setq y (cadr pt))
  (and (>= x min-x)
       (<= x max-x)
       (>= y min-y)
       (<= y max-y))
)

(defun process-shiyoukousei-block (obj logfh / result atts-variant atts-list att)
  (log-write logfh "  Block: FOUND")
  (log-write logfh
    (strcat "   Name: " (vla-get-Name obj))
  )

  (log-write logfh
    (strcat
      "   Position: "
      (rtos (car  (vlax-get obj 'InsertionPoint)) 2 3) ", "
      (rtos (cadr (vlax-get obj 'InsertionPoint)) 2 3) ", "
      (rtos (caddr(vlax-get obj 'InsertionPoint)) 2 3)
    )
  )

  ; 属性を持つ ブロック図があれば、属性値を削除
  (if (= :vlax-True (vla-get-HasAttributes obj))
    (progn
      (setq atts-variant (vla-GetAttributes obj))
      (setq atts-list
        (vlax-safearray->list
          (vlax-variant-value atts-variant)))
      (foreach att atts-list
        (log-write logfh
          (strcat
            "   ATTR "
            (vla-get-TagString att)
            " = "
            (vla-get-TextString att)
          )
        )
        (vla-put-TextString att "")
      )
    )
    (log-write logfh "   ATTR: No attributes")
  )
)

; ある座標の範囲に TEXTや MTEXTがあれば、削除
(defun find-and-delete-texts-in-range (blk logfh / obj txt-pos texts-to-delete)
  (log-write logfh "  TEXT objects in range (256.9,283)-(408,287.5):")

  (vlax-for obj blk
    (if (or (= (vla-get-ObjectName obj) "AcDbText")
            (= (vla-get-ObjectName obj) "AcDbMText"))
      (progn
        (setq txt-pos (vlax-get obj 'InsertionPoint))
        (if (point-in-range txt-pos 256.9 283.0 408.0 287.5)
          (progn
            (log-write logfh
              (strcat
                "   TEXT: "
                (vla-get-TextString obj)
              )
            )
            (log-write logfh
              (strcat
                "    Position: "
                (rtos (car  txt-pos) 2 3) ", "
                (rtos (cadr txt-pos) 2 3) ", "
                (rtos (caddr txt-pos) 2 3)
              )
            )
            (log-write logfh
              (strcat
                "    Layer: " (vla-get-Layer obj)
              )
            )
            (setq texts-to-delete (cons obj texts-to-delete))
          )
        )
      )
    )
  )

  (if texts-to-delete
    (progn
      (foreach txt texts-to-delete
        (vla-delete txt)
      )
      (log-write logfh
        (strcat "  Deleted "
                (itoa (length texts-to-delete))
                " TEXT object(s)")
      )
    )
    (log-write logfh "  No TEXT objects found in range")
  )

  (length texts-to-delete)
)

(defun conv-dwg
       ( dwgpath logfh
         / acad docs doc layouts lay blk
           obj att dwgname atts-variant atts-list result
           txt-pos texts-to-delete )

  (vl-load-com)

  (setq acad (vlax-get-acad-object))
  (setq docs (vla-get-Documents acad))

  (setq doc (vla-open docs dwgpath))
  (setq dwgname (vla-get-Name doc))

  (log-write logfh (strcat "DWG: " dwgname))

  (setq layouts (vla-get-Layouts doc))

  (vlax-for lay layouts
    (if (/= (vla-get-Name lay) "Model")
      (progn
        (setq texts-to-delete nil)
        (setq blk (vla-get-Block lay))

        (if blk
          (vlax-for obj blk
            (if (= (vla-get-ObjectName obj) "AcDbBlockReference")
              (if (= (vla-get-Name obj) "NS_ShiyouKousei_01")
                (progn
                  (process-shiyoukousei-block obj logfh)
                  (find-and-delete-texts-in-range blk logfh)
                )
              )
            )
          )
        )
      )
    )
  )

  (log-write logfh "----")
  (sleep-ms 1000)

  (vla-save doc)
  (vla-close doc :vlax-false)
)

pywinauto for python で visioを操作し、svgへ変換

pywinauto for python で AutoCADを操作し、DWGをDXFへ変換 - end0tknr's kipple - web写経開発

上記 entryのvisio版です

#!/usr/local/python3/bin/python3
# -*- coding: utf-8 -*-
import glob
import os
import pywinauto
import time
import subprocess
import psutil

def main():
    visio_paths = glob.glob( "VISIO/**/*", recursive=True)
    
    for i, visio_path in enumerate( visio_paths ) :
        ext = visio_path.split(".")[-1]
        if ext != "vsd" and ext != "vsdx":
            continue

        svg_path = visio_path.replace("."+ext, ".svg")
        if os.path.isfile( svg_path ):
            continue # svg作成済の場合、skip
        
        print(i, visio_path )
        subprocess.Popen([visio_path], shell=True)

        visio_win = find_app_win( "VISIO.EXE", ".* Visio Standard")
        if visio_win == None:
            print("ERROR fail find_app_win()" )
            continue
        # TODO - visioの起動完了を確認した上で、set_focus()したい
        time.sleep( 10 )
        visio_win.set_focus()

        # F12 = 名前を付けて保存
        pywinauto.keyboard.send_keys("{F12}")
        time.sleep(3)
        pywinauto.keyboard.send_keys("{TAB}")
        time.sleep(1)
        pywinauto.keyboard.send_keys("{DOWN 10}") # 保存形式: SVG選択
        time.sleep(1)
        pywinauto.keyboard.send_keys("{ENTER}")
        time.sleep(1)
        # 保存後にSVGを表示しない設定をon
        pywinauto.keyboard.send_keys("{TAB 3}")
        pywinauto.keyboard.send_keys("{SPACE}")
        # 保存実行
        pywinauto.keyboard.send_keys("{ENTER}")
        time.sleep(3)
        # TODO - visioの保存完了を確認した上で、終了したい
        pywinauto.keyboard.send_keys("%{F4}") # visio終了
        time.sleep(3)
        

def find_app_win( proc_name, win_name ):
    app_pid = None
    for i in range(20):
        for proc in psutil.process_iter(attrs=['pid', 'name']):
            if proc_name in proc.info['name']:
                app_pid = proc.info['pid']
                break
        if app_pid != None:
            break
        # 探索対象のアプリが、起動前の可能性もある為
        time.sleep(1)

    if app_pid == None:
        return
    app = pywinauto.Application().connect(process=app_pid)
    app_win = app.window( title_re=win_name )
    return app_win
        
if __name__ == '__main__':
    main()

「公用文作成の考え方」について(建議) by 文化庁

先日、entryに記載した「理科系の作文技術」と同様、文章の記載方法をまとめたもの。

記載内容は同様ですが、こちらは、令和4年(2022年)作成のためでしょうか、 読みやすい印象を受けます。

以下は p.8の抜粋