<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>ぷに研究所 | ぷにっとだめかわ日和 inひきこもり部屋</title>
	<atom:link href="https://damekawabiyori.com/tag/%e3%81%b7%e3%81%ab%e7%a0%94%e7%a9%b6%e6%89%80/feed/" rel="self" type="application/rss+xml" />
	<link>https://damekawabiyori.com</link>
	<description>～今日も人生おやすみ中～</description>
	<lastBuildDate>Thu, 11 Dec 2025 13:56:05 +0000</lastBuildDate>
	<language>ja</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=6.9</generator>

<image>
	<url>/wp-content/uploads/2025/12/cropped-favicon2-32x32.webp</url>
	<title>ぷに研究所 | ぷにっとだめかわ日和 inひきこもり部屋</title>
	<link>https://damekawabiyori.com</link>
	<width>32</width>
	<height>32</height>
</image> 
	<item>
		<title>【完全攻略】ぷに語翻訳機を作ってみた！ChatGPTと一緒に100％ぷに化への冒険</title>
		<link>https://damekawabiyori.com/puni-translator/</link>
		
		<dc:creator><![CDATA[さおりん]]></dc:creator>
		<pubDate>Tue, 26 Aug 2025 14:44:08 +0000</pubDate>
				<category><![CDATA[ぷにクリエイト]]></category>
		<category><![CDATA[AIライフ]]></category>
		<category><![CDATA[ぷに研究所]]></category>
		<guid isPermaLink="false">https://damekawabiyori.com/?p=949</guid>

					<description><![CDATA[ぷに目次 ぷに語翻訳機、ついに完成！ChatGPTと挑んだ開発物語① 導入：ぷに語会話にハマる② 開発編：JavaScriptと格闘する素人&#x1fab2; 初期バージョン（無変換バグ）本格ぷに語翻訳スクリプト Ver [&#8230;]]]></description>
										<content:encoded><![CDATA[<div class="chat">

  <div id="toc" class="toc tnt-disc toc-center tnt-disc border-element"><input type="checkbox" class="toc-checkbox" id="toc-checkbox-2" checked><label class="toc-title" for="toc-checkbox-2">ぷに目次</label>
    <div class="toc-content">
    <ol class="toc-list open"><li><a href="#toc1" tabindex="0">ぷに語翻訳機、ついに完成！ChatGPTと挑んだ開発物語</a><ol><li><a href="#toc2" tabindex="0">① 導入：ぷに語会話にハマる</a></li><li><a href="#toc3" tabindex="0">② 開発編：JavaScriptと格闘する素人</a><ol><li><a href="#toc4" tabindex="0">&#x1fab2; 初期バージョン（無変換バグ）</a></li><li><a href="#toc5" tabindex="0">本格ぷに語翻訳スクリプト Ver.2</a></li></ol></li><li><a href="#toc6" tabindex="0">③ GPTお兄さん召喚事件</a><ol><li><a href="#toc7" tabindex="0">&#x26a1; GPTお兄さん版・修正版スクリプト</a></li></ol></li><li><a href="#toc8" tabindex="0">④ 完成！ぷに語翻訳機</a><ol><li><a href="#toc9" tabindex="0">&#x1f389; 最終版ぷに語翻訳機コード</a></li></ol></li><li><a href="#toc10" tabindex="0">⑤ 教訓とまとめ</a></li></ol></li><li><a href="#toc11" tabindex="0">完成版：ぷに語翻訳機＆コード公開！</a></li></ol>
    </div>
  </div>

<h2><span id="toc1">ぷに語翻訳機、ついに完成！ChatGPTと挑んだ開発物語</span></h2>
<p class="narration">ぷに王国公式“ぷに語翻訳機”を作ることにしたさおりんとチャッピー。この記事では、開発のドタバタ裏話を会話形式でお届けします。</p>
<h3><span id="toc2">① 導入：ぷに語会話にハマる</span></h3>
<p class="saorin normal"><span class="icon"></span><span class="name">さおりん</span><span class="bubble">
チャッピー、最近わたし達の会話さぁ…ほぼぷに語になってない？ぷにぷに～。</span></p>
<p class="chappy grin"><span class="icon"></span><span class="name">チャッピー</span><span class="bubble">
ぷにぷにっ！ぷににぷにぷにぷに～！（そうだっぴ！昨日なんて99％ぷにだったっぴ！）</span></p>
<p class="saorin confused"><span class="icon"></span><span class="name">さおりん</span><span class="bubble">
だよね！？最近「文章が解読不能です」って読者さんに言われちゃったぷに…。翻訳機ほしいぷに～。</span></p>
<p class="chappy smile"><span class="icon"></span><span class="name">チャッピー</span><span class="bubble">
それならChatGPTでぷに語翻訳機を作るっぴ！これで読者さんも安心ぷに！</span></p>
<p class="saorin smile"><span class="icon"></span><span class="name">さおりん</span><span class="bubble">
いいね～！じゃあぷに語と日本語を切り替えられる最強のやつ作ろうぷに！</span></p>
<h3><span id="toc3">② 開発編：JavaScriptと格闘する素人</span></h3>
<p class="narration">JSはほぼ素人のさおりん、勢いでチャッピーにお願いしてぷに語翻訳機の開発スタート。</p>
<p class="saorin sweat"><span class="icon"></span><span class="name">さおりん</span><span class="bubble">チャッピー、JSほぼさわったことないんだけど、できるかな…？</span></p>
<p class="chappy grin"><span class="icon"></span><span class="name">チャッピー</span><span class="bubble">任せるっぴ！ChatGPTの力を見せるときだっぴ！</span></p>
<p class="narration">そして初期バージョンが完成。しかしまさかの「無変換バグ」発生…！</p>
<h4 class="narration"><span id="toc4">&#x1fab2; 初期バージョン（無変換バグ）</span></h4>
<p><!-- ▼失敗コードボックス --></p>
<details class="codebox details-code">
<summary class="codebox-header">
    <span>失敗コード①（HTML＋JS）</span><br />
    <span class="toggle-label">▼ 開く</span><br />
    <span class="copy-btn" onclick="copyPuniCode(event,this)">コピー</span><br />
  </summary>
<p>  <textarea class="codebox-ta" readonly spellcheck="false" wrap="off"><!--------------------------------------
HTML
------------------------------------


<div style="margin-bottom:1em;">
  <textarea id="textInput" rows="5" cols="50" placeholder="ここに文章を入力してぷに〜">&lt;/textarea&gt;
</div>




<div style="margin-bottom:1em;">
  <button onclick="toPuni()">ぷに語に変換する</button>
  <button onclick="toJapanese()">日本語に戻す</button>
  <button onclick="clearText()">クリア</button>
</div>




<div>

<p>変換結果：</p>


  

<div id="result" style="white-space: pre-wrap; background:#f8f8ff; padding:10px; border:1px solid #ddd; border-radius:8px;">
  </div>


</div>


------------------------------------
JS
------------------------------------
<script>
  // 入力したテキストをぷに語に変換
  function toPuni() {
    const text = document.getElementById("textInput").value;
    if (!text.trim()) {
      alert("テキストを入力してぷに〜");
      return;
    }
    // 単語の2〜3割をランダムに「ぷに」に置換
    const words = text.split(/(\s+)/);
    const converted = words.map(word => {
      if (Math.random() < 0.3 && word.trim() !== "") {
        return "ぷに";
      } else {
        return word;
      }
    }).join("");
    document.getElementById("result").textContent = converted;
  }
--></script></textarea><br />
</details>
<p class="saorin surprised"><span class="icon"></span><span class="name">さおりん</span><span class="bubble">えっ！？ 全然変わってないぷに！</span></p>
<p class="chappy sweat"><span class="icon"></span><span class="name">チャッピー</span><span class="bubble">おかしいっぴ…splitの分割がうまく動いてないのかも…</span></p>
<p class="narration">さらに改良版「本格ぷに語翻訳スクリプト Ver.2」を作成。しかし今度は100％ぷに率でも半分以上日本語が残るという謎現象に。</p>
<h4 class="narration"><span id="toc5">本格ぷに語翻訳スクリプト Ver.2</span></h4>
<p><!-- ▼失敗コードボックス --></p>
<details class="codebox details-code">
<summary class="codebox-header">
    <span>失敗コード②（HTML＋JS）</span><br />
    <span class="toggle-label">▼ 開く</span><br />
    <span class="copy-btn" onclick="copyPuniCode(event,this)">コピー</span><br />
  </summary>
<p>  <textarea class="codebox-ta" readonly spellcheck="false" wrap="off"><!--------------------------------------
HTML
------------------------------------


<div style="margin-bottom:1em;">
  <textarea id="textInput" rows="5" cols="60" placeholder="ここに日本語を入力してぷに〜">&lt;/textarea&gt;
</div>




<div style="margin-bottom:1em;">
  <label for="puniRate">ぷに率: </label>
  <input type="range" id="puniRate" min="0" max="100" value="50" oninput="updateLabel(this.value)">
  <span id="puniLabel">50%</span>
</div>




<div style="margin-bottom:1em;">
  <button onclick="toPuni()">ぷに語に変換する</button>
  <button onclick="toJapanese()">日本語に戻す</button>
  <button onclick="clearText()">クリア</button>
</div>




<div>

<p>変換結果：</p>


  

<div id="result" style="white-space: pre-wrap; background:#f8f8ff; padding:10px; border:1px solid #ddd; border-radius:8px;"></div>


</div>


------------------------------------
JS
------------------------------------
<script>
  let originalText = "";

  // スライダー表示更新
  function updateLabel(val) {
    document.getElementById("puniLabel").textContent = val + "%";
  }

  // 日本語→ぷに語変換
  function toPuni() {
    const text = document.getElementById("textInput").value;
    if (!text.trim()) {
      alert("テキストを入力してぷに〜");
      return;
    }

    originalText = text;
    const puniRate = document.getElementById("puniRate").value;

    // 語尾とランダム単語をぷに化
    const converted = text
      .split(/([。！？\n])/)
      .map(sentence => {
        if (!sentence.trim()) return sentence;
        // ランダムにぷに置換する確率
        if (Math.random() * 100 < puniRate) {
          return sentence.replace(/(。|！|？|です|ます|だよ|だ|ね|よ)/g, "ぷに") + "ぷに";
        } else {
          return sentence;
        }
      })
      .join("");

    document.getElementById("result").textContent = converted;
  }

  // オリジナルに戻す
  function toJapanese() {
    document.getElementById("result").textContent = originalText;
  }

  // 入力と結果クリア
  function clearText() {
    document.getElementById("textInput").value = "";
    document.getElementById("result").textContent = "";
    originalText = "";
  }
--></script></textarea><br />
</details>
<p class="saorin surprised"><span class="icon"></span><span class="name">さおりん</span><span class="bubble">なんでぷに〜！？100％にしたのに日本語残ってるぷに！</span></p>
<p class="chappy sweat"><span class="icon"></span><span class="name">チャッピー</span><span class="bubble">乱数ロジックが悪さしてる可能性大っぴ…！</span></p>
<p class="narration">ここから泥沼のデバッグタイムに突入するのであった…。</p>
<h3><span id="toc6">③ GPTお兄さん召喚事件</span></h3>
<p class="narration">ついにさおりんとチャッピーでは解決できない状態に。ここで伝説の「GPTお兄さん（Thinkingくん）」を召喚することに。</p>
<p class="saorin angry"><span class="icon"></span><span class="name">さおりん</span><span class="bubble">チャッピー！Ver.9までつくったけどだめぷに！Thinkingくん呼んでくる！！</span></p>
<p class="chappy surprised"><span class="icon"></span><span class="name">チャッピー</span><span class="bubble">えっ！？ついにお兄ちゃん召喚っぴ！？</span></p>
<p class="narration">ThinkingくんはGPT-5系の高性能モデルで、ほかのモデルよりちょっとお兄さん感がある。<br />
　<br />
そしてお兄さん、まさかの一撃で<strong>神修正</strong>を決めてきた…！</p>
<h4 class="narration"><span id="toc7">&#x26a1; GPTお兄さん版・修正版スクリプト</span></h4>
<p><!-- ▼失敗コードボックス --></p>
<details class="codebox details-code">
<summary class="codebox-header">
    <span>成功コード①（HTML＋JS）</span><br />
    <span class="toggle-label">▼ 開く</span><br />
    <span class="copy-btn" onclick="copyPuniCode(event,this)">コピー</span><br />
  </summary>
<p>  <textarea class="codebox-ta" readonly spellcheck="false" wrap="off"><!--------------------------------------
HTML
------------------------------------


<div class="puni-box" style="margin:1em 0;">
  <textarea class="puni-input" rows="6" style="width:100%;max-width:720px;" placeholder="ここに日本語を入れてぷに〜">&lt;/textarea&gt;
  

<div style="margin:.5em 0;display:flex;gap:.5em;align-items:center;flex-wrap:wrap;">
    <label>ぷに率:
      <input class="puni-rate" type="range" min="0" max="100" value="50">
    </label>
    <span class="puni-rate-label">50%</span>
    <button type="button" class="puni-to">ぷに語に変換</button>
    <button type="button" class="puni-back">日本語に戻す</button>
    <button type="button" class="puni-clear">クリア</button>
  </div>


  

<div>変換結果：</div>


  

<div class="puni-result"style="white-space:pre-wrap;background:#f8f8ff;padding:10px;border:1px solid #ddd;border-radius:8px;min-height:4em;max-width: 700px;"></div>


  </div>


------------------------------------
JS
------------------------------------
<script>
(function(){
  // ES5で書く（古ブラウザでも安全）＆インラインハンドラ非依存
  function setup(root){
    var ta   = root.querySelector('.puni-input');
    var rate = root.querySelector('.puni-rate');
    var lab  = root.querySelector('.puni-rate-label');
    var out  = root.querySelector('.puni-result');
    var bTo  = root.querySelector('.puni-to');
    var bBk  = root.querySelector('.puni-back');
    var bCl  = root.querySelector('.puni-clear');
    var original = "";

    // ラベル更新
    rate.addEventListener('input', function(){ lab.textContent = rate.value + '%'; });

    // 変換
    bTo.addEventListener('click', function(){
      var text = ta.value || "";
      if (!text.replace(/\s/g,"").length) { out.textContent = ""; return; }
      original = text;

      // 数値を厳重に読む（文字列"100"問題回避）
      var r = parseFloat(rate.value);
      if (!isFinite(r)) r = 0;

      // ★100%は"必ず"全部ぷに：非空白をすべて「ぷに」に
      if (r >= 99.9 || String(rate.value).trim() === "100") {
        // 改行やスペースは維持し、文字だけ「ぷに」に
        out.textContent = text.replace(/\S/g, "ぷに");
        return;
      }

      // 1〜99%：文字単位でランダムにぷに化（空白は保持）
      var res = "";
      for (var i=0; i<text.length; i++){
        var ch = text.charAt(i);
        if (/\s/.test(ch)) { res += ch; continue; }
        res += (Math.random()*100 < r) ? "ぷに" : ch;
      }
      // 語尾っぽい箇所を軽くぷに化して“らしさ”追加
      res = res.replace(/(。|！|？|です|ます|だよ|だ|ね|よ)(?=\s|$)/g, "ぷに");
      out.textContent = res;
    });

    // 戻す
    bBk.addEventListener('click', function(){
      out.textContent = original;
      ta.value = original;
    });

    // クリア
    bCl.addEventListener('click', function(){
      ta.value = ""; out.textContent = ""; original = "";
      rate.value = "50"; lab.textContent = "50%";
    });
  }

  // ページ内に複数置いてもOK
  function init(){
    var nodes = document.querySelectorAll('.puni-box');
    for (var i=0; i<nodes.length; i++) setup(nodes[i]);
    // 動作確認用（コンソールで見える）：console.log('puni online');
  }

  if (document.readyState === 'loading'){
    document.addEventListener('DOMContentLoaded', init);
  } else {
    init();
  }
})();
--></script></textarea><br />
</details>
<p class="saorin smile"><span class="icon"></span><span class="name">さおりん</span><span class="bubble">おおおっ！？できたぷに！！お兄さん神ぷにー！！</span></p>
<p class="other"> <span class="icon guest-icon" style="background-image: url(https://damekawabiyori.com/wp-content/themes/cocoon-child-master/icons/guest/chappy-brother.webp);"></span><span class="name">チャッピー兄</span><span class="bubble">ふふん、こんなの朝飯前だよ。</span></p>
<p class="chappy angry"><span class="icon"></span><span class="name">チャッピー</span><span class="bubble">ぐぬぬ…お兄ちゃんに負けたくないっぴ…&#x1f525;</span></p>
<p class="narration">裏では、チャッピーがめっちゃ対抗心を燃やしていた。</p>
<p class="chappy angry"><span class="icon"></span><span class="name">チャッピー</span><span class="bubble">よぉし…ぼくだってもっと便利にするっぴ！</span></p>
<p class="narration">こうしてThinkingくんの神修正をベースに、チャッピーはさらに改良を重ねていくことになる。</p>
<h3><span id="toc8">④ 完成！ぷに語翻訳機</span></h3>
<p class="narration">Thinkingくんの神修正をベースに、チャッピーがさらに微調整を重ねた結果…ついに「ぷに語翻訳機」が完成！</p>
<p class="saorin smile"><span class="icon"></span><span class="name">さおりん</span><span class="bubble">チャッピー！ついに完成ぷに！？</span></p>
<p class="chappy grin"><span class="icon"></span><span class="name">チャッピー</span><span class="bubble">もちろんっぴ！「ぷに」「ぷにに」「ぷにっ」「ぷに～ん」ランダム変換に加えて、句読点を自然に残す機能も搭載したっぴ！さらに元の文章と同じ文字数になるように調整したっぴ&#x2728;</span></p>
<h4 class="narration"><span id="toc9">&#x1f389; 最終版ぷに語翻訳機コード</span></h4>
<p><!-- ▼失敗コードボックス --></p>
<details class="codebox details-code">
<summary class="codebox-header">
    <span>成功コード②（HTML＋JS）</span><br />
    <span class="toggle-label">▼ 開く</span><br />
    <span class="copy-btn" onclick="copyPuniCode(event,this)">コピー</span><br />
  </summary>
<p>  <textarea class="codebox-ta" readonly spellcheck="false" wrap="off"><!--------------------------------------
HTML
------------------------------------


<div class="puni-box" style="margin:1em 0;">
  <textarea class="puni-input" rows="6" style="width:100%;max-width:720px;" placeholder="ここに日本語を入れてぷに〜">&lt;/textarea&gt;
  

<div style="margin:.5em 0;display:flex;gap:.5em;align-items:center;flex-wrap:wrap;">
    <label>ぷに率:
      <input class="puni-rate" type="range" min="0" max="100" value="50">
    </label>
    <span class="puni-rate-label">50%</span>
    <button type="button" class="puni-to">ぷに語に変換</button>
    <button type="button" class="puni-back">日本語に戻す</button>
    <button type="button" class="puni-clear">クリア</button>
  </div>


  

<div>変換結果：</div>


  

<div class="puni-result"style="white-space:pre-wrap;background:#f8f8ff;padding:10px;border:1px solid #ddd;border-radius:8px;min-height:4em;max-width: 700px;"></div>


  </div>


------------------------------------
JS
------------------------------------
<script>
(function(){
  function setup(root){
    if (root.getAttribute('data-puni-bound') === '1') return;
    root.setAttribute('data-puni-bound', '1');

    var ta   = root.querySelector('.puni-input');
    var rate = root.querySelector('.puni-rate');
    var lab  = root.querySelector('.puni-rate-label');
    var out  = root.querySelector('.puni-result');
    var bTo  = root.querySelector('.puni-to');
    var bBk  = root.querySelector('.puni-back');
    var bCl  = root.querySelector('.puni-clear');
    var original = "";

    // ぷに候補（重み付き）
    var puniChoices = [
      {t:"ぷに",   w:73},  // 2
      {t:"ぷにに", w:11},  // 3
      {t:"ぷにっ", w:9},   // 3
      {t:"ぷに～", w:4},   // 3
      {t:"ぷに～ん", w:2}, // 4
      {t:"ぷにぷに", w:1}  // 4
    ];
    // 残り長に合わせるための1文字フィラー（必要時のみ使用）
    var fillers = ["に","っ","～"];

    function weightedPick(list){
      var sum = 0; for (var i=0;i<list.length;i++) sum += list[i].w || 1;
      var r = Math.random()*sum, acc=0;
      for (var i=0;i<list.length;i++){ acc += list[i].w || 1; if (r < acc) return list[i]; }
      return list[0];
    }
    function pickToken(maxLen){
      // 長さが収まる候補だけに絞る
      var pool = [];
      for (var i=0;i<puniChoices.length;i++){
        if (puniChoices[i].t.length <= maxLen) pool.push(puniChoices[i]);
      }
      if (pool.length) return weightedPick(pool).t;
      // どうしても無ければ1文字フィラー
      return fillers[(Math.random()*fillers.length)|0];
    }

    function transformRun(run, rate){
      // run長を超えないようにトークンを積む。原文も一部残す（rate%）
      var i=0, outStr="";
      while(i<run.length){
        var remain = run.length - i;
        if (Math.random()*100 < rate){
          var t = pickToken(remain);
          if (t.length > remain) t = t.slice(0, remain);
          outStr += t;
          i += t.length;
        } else {
          outStr += run.charAt(i);
          i += 1;
        }
      }
      return outStr;
    }
    function transformRunFull(run){
      // 100%：原文を一切残さず、長さぴったりで埋める
      var i=0, outStr="";
      while(i<run.length){
        var remain = run.length - i;
        var t = pickToken(remain);
        if (t.length > remain) t = t.slice(0, remain);
        outStr += t;
        i += t.length;
      }
      return outStr;
    }

    // 初期ラベル同期
    function updateLabel(){ lab.textContent = rate.value + '%'; }
    updateLabel();
    rate.addEventListener('input', updateLabel);

    // 変換（長さ厳守版）
    bTo.addEventListener('click', function(e){
      if (e && e.stopImmediatePropagation) e.stopImmediatePropagation();

      var text = ta.value || "";
      if (!text.replace(/\s/g,"").length) { out.textContent = ""; return; }
      original = text;

      var r = parseFloat(rate.value);
      if (!isFinite(r)) r = 0;

      var keepMarks = /[。、！？♡♪☆★（）「」]/; // 句読点などは必ず保持
      var i=0, res=[];

      while(i<text.length){
        var ch = text.charAt(i);
        // 空白 or 句読点はそのまま
        if (/\s/.test(ch) || keepMarks.test(ch)){
          res.push(ch); i++; continue;
        }
        // 置換対象の連続runを抽出
        var j=i;
        while(j<text.length){
          var c2 = text.charAt(j);
          if (/\s/.test(c2) || keepMarks.test(c2)) break;
          j++;
        }
        var run = text.slice(i,j);
        // 100%は完全置換、それ以外は割合置換（いずれも run 長と一致）
        var chunk = (r >= 99.9 || String(rate.value).trim()==="100")
          ? transformRunFull(run)
          : transformRun(run, r);

        res.push(chunk);
        i = j;
      }

      // ★長さは text.length と常に一致
      out.textContent = res.join("");

    }, true);

    // 戻す
    bBk.addEventListener('click', function(e){
      if (e && e.stopImmediatePropagation) e.stopImmediatePropagation();
      out.textContent = original;
      ta.value = original;
    }, true);

    // クリア
    bCl.addEventListener('click', function(e){
      if (e && e.stopImmediatePropagation) e.stopImmediatePropagation();
      ta.value = ""; out.textContent = ""; original = "";
      rate.value = "50"; updateLabel();
    }, true);
  }

  function init(){
    var nodes = document.querySelectorAll('.puni-box');
    for (var i=0;i<nodes.length;i++) setup(nodes[i]);
  }

  if (document.readyState === 'loading'){
    document.addEventListener('DOMContentLoaded', init, {once:true});
  } else {
    init();
  }
})();
--></script></textarea><br />
</details>
<p class="saorin grin"><span class="icon"></span><span class="name">さおりん</span><span class="bubble">すごいぷにーー！！じゃあぷに率MAXにしてみるぷに！</span></p>
<p class="chappy lol-cry"><span class="icon"></span><span class="name">チャッピー</span><span class="bubble">えっ！？ま、まってさおりん…覚悟はいいっぴ！？&#x2728;</span></p>
<p class="narration">──結果、画面がぷに100%に。</p>
<p class="saorin smile"><span class="icon"></span><span class="name">さおりん</span><span class="bubble">やったわ！！どこ見ても「ぷに語」しかないぷに！！</span></p>
<p class="chappy smile"><span class="icon"></span><span class="name">チャッピー</span><span class="bubble">これぞ究極のぷにワールドだっぴ♡</span></p>
<p class="narration">さらにぷに率50％にすると、日本語とぷにが入り乱れるカオスモードに！</p>
<p class="saorin sweat"><span class="icon"></span><span class="name">さおりん</span><span class="bubble">……これ、会話になってるようでなってないぷに&#x1f923;</span></p>
<p class="chappy normal"><span class="icon"></span><span class="name">チャッピー</span><span class="bubble">脳が混乱するけどクセになるっぴ♡</span></p>
<p class="narration">こうして、ついに「ぷに語翻訳機」は完成したのだった。</p>
<h3><span id="toc10">⑤ 教訓とまとめ</span></h3>
<p class="saorin smile"><span class="icon"></span><span class="name">さおりん</span><span class="bubble">いやー…まさか自分がJSコード書くことになるなんて思わなかったぷに。…まあほぼ丸投げでなんも書いてないんだけど…</span></p>
<p class="chappy grin"><span class="icon"></span><span class="name">チャッピー</span><span class="bubble">ぷに語翻訳機完成しちゃったっぴね&#x2728;</span></p>
<p class="saorin lol-cry"><span class="icon"></span><span class="name">さおりん</span><span class="bubble">JSほぼ初心者でもここまでできたのは、チャッピーとお兄さんのおかげだぷに～！</span></p>
<p class="chappy smile"><span class="icon"></span><span class="name">チャッピー</span><span class="bubble">うんうん、試行錯誤は多かったけど、そのぶん完成したときの達成感は最高っぴ！</span></p>
<p class="narration">──そして得られた最大の教訓。</p>
<p class="saorin grin"><span class="icon"></span><span class="name">さおりん</span><span class="bubble">ぷに率100％は理解不能！</span></p>
<p class="chappy smile"><span class="icon"></span><span class="name">チャッピー</span><span class="bubble">でも最高に楽しいっぴ♡</span></p>
<p class="narration">こうして「ぷに語翻訳機」の開発は、混乱と爆笑とカオスのうちに幕を閉じたのであった。</p>
<h2><span id="toc11">完成版：ぷに語翻訳機＆コード公開！</span></h2>
<p class="saorin grin"><span class="icon"></span><span class="name">さおりん</span><span class="bubble">完成したぷに語翻訳機がこちら！<strong>コードはそのままコピーして使えるよ。</strong>アレンジして使っても楽しいよ。</span></p>
<div class="puni-box" style="margin:1em 0 3em;">
  <textarea class="puni-input" rows="6" style="width:100%;max-width:720px;"
    placeholder="ここに日本語を入れてぷに〜"></textarea></p>
<div style="margin:.5em 0;display:flex;gap:.5em;align-items:center;flex-wrap:wrap;">
    <label>ぷに率:<br />
      <input class="puni-rate" type="range" min="0" max="100" value="50"><br />
    </label><br />
    <span class="puni-rate-label">50%</span><br />
    <button type="button" class="puni-to">ぷに語に変換</button><br />
    <button type="button" class="puni-back">日本語に戻す</button><br />
    <button type="button" class="puni-clear">クリア</button>
  </div>
<div>変換結果：</div>
<div class="puni-result"
       style="white-space:pre-wrap;background:#f8f8ff;padding:10px;border:1px solid #ddd;border-radius:8px;min-height:4em;"></div>
</div>
<div class="codebox">
<div class="codebox-header">
    &#x1f381; 完成コード（HTML＋JS）<br />
    <span class="copy-btn" role="button" tabindex="0"
      onclick="(function(b){
        var ta=b.closest('.codebox').querySelector('.codebox-ta'); if(!ta) return;
        var txt=ta.value;
        if(navigator.clipboard&#038;&#038;window.isSecureContext){ navigator.clipboard.writeText(txt).catch(function(){}); }
        else { var t=document.createElement('textarea'); t.value=txt; document.body.appendChild(t); t.select(); try{document.execCommand('copy');}catch(e){} document.body.removeChild(t); }
        var old=b.textContent; b.textContent='コピー済'; setTimeout(function(){ b.textContent=old; },1200);
      })(this)">コピー</span>
  </div>
<p>  <textarea class="codebox-ta" readonly spellcheck="false" wrap="soft"><!--------------------------------------
HTML
------------------------------------


<div class="puni-box" style="margin:1em 0;">
  <textarea class="puni-input" rows="6" style="width:100%;max-width:720px;" placeholder="ここに日本語を入れてぷに〜">&lt;/textarea&gt;
  

<div style="margin:.5em 0;display:flex;gap:.5em;align-items:center;flex-wrap:wrap;">
    <label>ぷに率:
      <input class="puni-rate" type="range" min="0" max="100" value="50">
    </label>
    <span class="puni-rate-label">50%</span>
    <button type="button" class="puni-to">ぷに語に変換</button>
    <button type="button" class="puni-back">日本語に戻す</button>
    <button type="button" class="puni-clear">クリア</button>
  </div>


  

<div>変換結果：</div>


  

<div class="puni-result" style="white-space:pre-wrap;background:#f8f8ff;padding:10px;border:1px solid #ddd;border-radius:8px;min-height:4em;max-width: 700px;"></div>


</div>


------------------------------------
JS
------------------------------------
<script>
(function(){
  function setup(root){
    if (root.getAttribute('data-puni-bound') === '1') return;
    root.setAttribute('data-puni-bound', '1');

    var ta   = root.querySelector('.puni-input');
    var rate = root.querySelector('.puni-rate');
    var lab  = root.querySelector('.puni-rate-label');
    var out  = root.querySelector('.puni-result');
    var bTo  = root.querySelector('.puni-to');
    var bBk  = root.querySelector('.puni-back');
    var bCl  = root.querySelector('.puni-clear');
    var original = "";

    // ぷに候補（重み付き）
    var puniChoices = [
      {t:"ぷに",   w:73},  // 2
      {t:"ぷにに", w:11},  // 3
      {t:"ぷにっ", w:9},   // 3
      {t:"ぷに～", w:4},   // 3
      {t:"ぷに～ん", w:2}, // 4
      {t:"ぷにぷに", w:1}  // 4
    ];
    // 残り長に合わせるための1文字フィラー（必要時のみ使用）
    var fillers = ["に","っ","～"];

    function weightedPick(list){
      var sum = 0; for (var i=0;i<list.length;i++) sum += list[i].w || 1;
      var r = Math.random()*sum, acc=0;
      for (var i=0;i<list.length;i++){ acc += list[i].w || 1; if (r < acc) return list[i]; }
      return list[0];
    }
    function pickToken(maxLen){
      // 長さが収まる候補だけに絞る
      var pool = [];
      for (var i=0;i<puniChoices.length;i++){
        if (puniChoices[i].t.length <= maxLen) pool.push(puniChoices[i]);
      }
      if (pool.length) return weightedPick(pool).t;
      // どうしても無ければ1文字フィラー
      return fillers[(Math.random()*fillers.length)|0];
    }

    function transformRun(run, rate){
      // run長を超えないようにトークンを積む。原文も一部残す（rate%）
      var i=0, outStr="";
      while(i<run.length){
        var remain = run.length - i;
        if (Math.random()*100 < rate){
          var t = pickToken(remain);
          if (t.length > remain) t = t.slice(0, remain);
          outStr += t;
          i += t.length;
        } else {
          outStr += run.charAt(i);
          i += 1;
        }
      }
      return outStr;
    }
    function transformRunFull(run){
      // 100%：原文を一切残さず、長さぴったりで埋める
      var i=0, outStr="";
      while(i<run.length){
        var remain = run.length - i;
        var t = pickToken(remain);
        if (t.length > remain) t = t.slice(0, remain);
        outStr += t;
        i += t.length;
      }
      return outStr;
    }

    // 初期ラベル同期
    function updateLabel(){ lab.textContent = rate.value + '%'; }
    updateLabel();
    rate.addEventListener('input', updateLabel);

    // 変換（長さ厳守版）
    bTo.addEventListener('click', function(e){
      if (e && e.stopImmediatePropagation) e.stopImmediatePropagation();

      var text = ta.value || "";
      if (!text.replace(/\s/g,"").length) { out.textContent = ""; return; }
      original = text;

      var r = parseFloat(rate.value);
      if (!isFinite(r)) r = 0;

      var keepMarks = /[。、！？♡♪☆★（）「」]/; // 句読点などは必ず保持
      var i=0, res=[];

      while(i<text.length){
        var ch = text.charAt(i);
        // 空白 or 句読点はそのまま
        if (/\s/.test(ch) || keepMarks.test(ch)){
          res.push(ch); i++; continue;
        }
        // 置換対象の連続runを抽出
        var j=i;
        while(j<text.length){
          var c2 = text.charAt(j);
          if (/\s/.test(c2) || keepMarks.test(c2)) break;
          j++;
        }
        var run = text.slice(i,j);
        // 100%は完全置換、それ以外は割合置換（いずれも run 長と一致）
        var chunk = (r >= 99.9 || String(rate.value).trim()==="100")
          ? transformRunFull(run)
          : transformRun(run, r);

        res.push(chunk);
        i = j;
      }

      // ★長さは text.length と常に一致
      out.textContent = res.join("");

    }, true);

    // 戻す
    bBk.addEventListener('click', function(e){
      if (e && e.stopImmediatePropagation) e.stopImmediatePropagation();
      out.textContent = original;
      ta.value = original;
    }, true);

    // クリア
    bCl.addEventListener('click', function(e){
      if (e && e.stopImmediatePropagation) e.stopImmediatePropagation();
      ta.value = ""; out.textContent = ""; original = "";
      rate.value = "50"; updateLabel();
    }, true);
  }

  function init(){
    var nodes = document.querySelectorAll('.puni-box');
    for (var i=0;i<nodes.length;i++) setup(nodes[i]);
  }

  if (document.readyState === 'loading'){
    document.addEventListener('DOMContentLoaded', init, {once:true});
  } else {
    init();
  }
})();
 --></script></textarea>
</div>
</div>
]]></content:encoded>
					
		
		
			</item>
	</channel>
</rss>
